Golang编程
2025-04-17 22:01:31 0 举报
AI智能生成
基础语法、进阶知识、底层原理
作者其他创作
大纲/内容
基础部分
变量
变量
特定的名字 -> 特定位置的内存块
声明方式1:var,默认零值
整型:0
浮点型:0.0
布尔型:false
字符串:“”
指针、接口、切片、channel、map和函数类型:nil
声明方式2::=,只能在函数内部使用
常量
编译期间创建,运行期间不变
声明方式:const
作用域
针对标识符的限制,指这个标识符在声明后可以被有效使用的代码区域,编译器在编译过程中会进行检查,不符合则报错
变量遮蔽
内层代码块声明了与外层代码块同名切类型相同的变量
内层代码块中内层变量替换外层变量
占位符
%d:十进制整数
%f:浮点数
%s:字符串
%t:布尔值
%v:通用格式化标识,根据值的类型进行格式化
%p:指针地址
%b:二进制表示
%o:八进制表示
%x:十六进制表示,小写
%X:十六进制表示,大写
%c:字符
%q:带引号的字符串
%e:科学计数法的浮点数,小写e
%E:科学计数法的浮点数,大写E
%g:根据实际情况使用%f或%e
%G:根据实际情况使用%f或%E
数据类型
整型
平台有关型
int、uint
平台无关型
int8....int64
uint8.....uint64
uintptr、byte
整型的溢出问题:超过了存储的数值范围
类型转换
strconv.Itoa()
strconv.Atoi()
浮点型
类型:float32 float64
类型转换
strconv.FormatFolat()
复数类型
complex
real
imag
自定义类型
基本用法
type MyInt int
流程控制
运算符
^: 不一样为1
if
switch
fallthrough:执行下一个case
for
书写方式
for init; condition; post {}
for condition {}
for {}
for key, value := range sl {}
for i := range sl {}
for _, v := range sl {}
for range sl {}
continue
continue tag
break
break tag
遍历 map 中的元素具有随机性
函数
代码复用、特定输入、执行结果
函数声明
func 函数名(参数) (返回值){
// 函数体
}
// 函数体
}
函数名第一个字母不能是数字
函数名首字母大写代表函数被导出
支持变长参数:...type
只要函数名相同两个函数就是同一类型
即不支持函数重载
值传递的方式
将实际参数在内存中的表示逐位拷贝到形式参数中
string、slice、map这些拷贝的就是数据内容的描述符,和数据大小无关
即浅拷贝
返回值
支持多返回值
高阶函数
函数做形参
函数做返回值
匿名函数
没有具体名称的函数
defer
用于延迟执行函数调用的关键字,无论执行成功还是失败都会执行
执行顺序是后进先出的,因此特别适合资源的清理和释放
使用场景:文件的关闭、锁的释放等等
函数的返回值在return语句执行时已经确定了
看defer中的数值变化是否与返回变量有关
内置函数
new:为指定类型分配内存并返回指向该类型的指针
len:返回字符串、数组、切片、字典、通道的长度
cap:返回切片的容量或通道缓冲区的大小
为什么不支持map?
map特殊数据存储方式
make用于创建slice、map、chan
append:将元素追加到切片的末尾
delete:从map中删除指定key的键值对
copy:将源切片复制到目标切片
close:关闭一个通道
panic:触发一个运行时错误
recover:从panic中恢复,用于处理运行时错误
方法
本质:一个以所属类型参数作为第一个参数的普通函数
基本和函数一样,但存在一个方法所属类型(也只能存在一个)
func (t *T或T) MethodName(参数列表) (返回值列表) {
// 方法体
}
// 方法体
}
所属类型的基类型不能是指针类型和接口类型
type MyInt *int
func (r MyInt) String() string { // r的基类型为MyInt,编译器报错:invalid receiver type MyInt (MyInt is a pointer type)
return fmt.Sprintf("%d", *(*int)(r))
}
func (r MyInt) String() string { // r的基类型为MyInt,编译器报错:invalid receiver type MyInt (MyInt is a pointer type)
return fmt.Sprintf("%d", *(*int)(r))
}
type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) { // r的基类型为MyReader,编译器报错:invalid receiver type MyReader (MyReader is an interface type)
return r.Read(p)
}
func (r MyReader) Read(p []byte) (int, error) { // r的基类型为MyReader,编译器报错:invalid receiver type MyReader (MyReader is an interface type)
return r.Read(p)
}
不能为原生类型添加方法
不能跨越包去声明新方法
允许值类型调用指针接收者的方法,它会自动取地址 &p
方法集合
某所属类型的方法集合和某个接口的方法集合相同,或是这个接口方法集合的超集
即所属类型实现了这个接口
接口
一组方法集合,一种定义对象行为的方式
任何实现了这组方法集合的类型都被视为实现了该接口
方法名称具名且唯一
type 接口名称 interface {
method(参数列表) 返回值列表
method2(参数列表) 返回值列表
method3(参数列表) 返回值列表
...
methodN(参数列表) 返回值列表
}
method(参数列表) 返回值列表
method2(参数列表) 返回值列表
method3(参数列表) 返回值列表
...
methodN(参数列表) 返回值列表
}
空接口
没有一个方法的接口
任何类型都可以被赋值给该空接口类型的变量,任何类型都满足空接口类型的方法集合
尽量定义小接口
利于复用和组合
静态类型
编译器在编译期间会对接口类型变量的赋值进行类型检查,确保右值实现了接口的所有方法
动态类型
接口类型变量在运行时存储了右值的真实类型信息
一切皆组合
垂直组合
水平组合
结构体
一系列(0或多)具有相同类型或不同类型的数据构成的数据集合
type Empty struct{}
空结构体大小占用为0
type T struct {
Field1 T1
Field2 T2
... ...
FieldN Tn
}
Field1 T1
Field2 T2
... ...
FieldN Tn
}
字段有自己类型和值
字段名称唯一
首字母决定可访问性
自定义类型和类型别名
自定义类型:type T S
与底层类型没有关系,不能直接赋值和比较
类型别名:type T = S
与底层类型完全相同,能直接赋值和比较
变量声明和初始化
声明
var p Person
默认零值
初始化
p := Person{
Name: "Alice",
Age: 25,
}
Name: "Alice",
Age: 25,
}
伪继承
结构体组合
多态
不同类型实现该接口
差异性
组合限制:无法直接访问嵌入结构体的私有字段和私有方法
方法重写:子类可以重写嵌入结构体的方法
父子类型转换:需要显式的类型断言和转换操作
结构体字段标签
一种用于为结构体字段附加元数据的机制
为结构体字段提供额外的信息
通过反射机制读取和解析,在运行时根据标签信息进行相应的处理
使用场景:序列化和反序列化、数据库的映射、表单验证
json
xml
csv
Context
目的
为了在不同的goroutine之间或跨api边界传递超时、取消信号和其他请求范围内的值:用户身份信息、请求处理日志、跟踪信息
提供了一个用于跨api边界传递超时、取消信号和其他请求范围值的通用数据结构
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
核心方法
Deadline():返回context的截止时间,表示在这个时间点之后context会自动取消
如果没有设置,则返回时间零值和false
deadline, ok := ctx.Deadline()
if ok {
// Context 有截止时间
} else {
// Context 没有截止时间
}
if ok {
// Context 有截止时间
} else {
// Context 没有截止时间
}
Done():返回一个只读通道,当context被取消时,该通道会被关闭;可以通过监听这个来检测context是否被取消
如果context永不取消,则返回nil
select {
case <-ctx.Done():
// Context 已取消
default:
// Context 尚未取消
}
case <-ctx.Done():
// Context 已取消
default:
// Context 尚未取消
}
Err()
返回方法取消时产生的错误
如果context尚未取消,该方法返回nil
if err := ctx.Err(); err != nil {
// Context 已取消,处理错误
}
// Context 已取消,处理错误
}
Value()
返回与context关联的键值对,一般用于在goroutine之间传递请求范围内的信息
如果没有关联的值,则返回nil
value := ctx.Value(key)
if value != nil {
// 存在关联的值
}
if value != nil {
// 存在关联的值
}
创建方法
context.Background():返回一个非nil的空context,没有携带任何值,也没有取消和超时信号
通常作为根context使用
context.TODO():返回一个非nil的空context,没有携带任何值,也没有取消和超时信号
不确定上下文时使用
context.WithValue():接受父context和一个键值对,返回一个新的携带这个键值对的context
context.WithCancel():接受父context,返回一个新的context和一个取消函数
当取消函数被调用时,子context会被取消,同时会向context关联的Done()通道发送取消信号,所有衍生的context都会被取消
适用于手动取消的场景
context.WithCancelCause():类似于WithCancel,增加了可以额外设置取消原因,调用时需要传入一个error参数
context.Cause():用于返回取消context的原因
若是调用WithCancelCause的取消函数进行的取消操作,则可以获得error的值,反之获得c.Err的值
如果context尚未取消,则返回nil
context.WithDeadline():接受父context和截止时间戳,返回一个新的context;当时间到达,其衍生context会被自动取消
适用于需要在特定时间点取消操作的场景
contex.WithTimeout():功能同WithDeadline,底层调用该函数,只不过第二个参数接受的是一个超时时间,而不是截止时间
使用需要在一段时间后取消操作的场景
使用场景
传递共享数据
ctx := context.WithValue(req.Context(), requestIDKey, requestID)
传递取消信号,结束任务
ctx, cancelFunc := context.WithCancel(context.Background())
go Working(ctx)
time.Sleep(3 * time.Second)
cancelFunc()
go Working(ctx)
time.Sleep(3 * time.Second)
cancelFunc()
超时控制
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 在另一个 goroutine 中执行耗时操作
go func() {
// 模拟一个耗时的操作,例如数据库查询
time.Sleep(5 * time.Second)
cancel()
}()
defer cancel()
// 在另一个 goroutine 中执行耗时操作
go func() {
// 模拟一个耗时的操作,例如数据库查询
time.Sleep(5 * time.Second)
cancel()
}()
使用规则
不要把context嵌入结构体中
永远不要传递nil的context
仅用于传输进程和api的请求作用域数据,不要用于向函数传递可选参数
反射
作用
允许程序在运行时检查和修改变量的类型和值
可以编写更加灵活、适配性更好的代码
核心类型
Type:表示go语言中每种类型的类型信息
Value:表示值的接口,可以对值进行读取和修改
使用场景
类型检查:在运行时确定变量的具体类型
动态访问:获取和设置结构体字段的值
函数和方法调用:在运行时调用方法或函数
处理接口:当变量是接口时,反射可以用来断言下其实际类型
基本操作
获取类型和值
var x float64 = 3.4
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println("Type:", t) // 输出: Type: float64
fmt.Println("Value:", v.Interface()) // 输出: Value: 3.4
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println("Type:", t) // 输出: Type: float64
fmt.Println("Value:", v.Interface()) // 输出: Value: 3.4
修改值
v.SetFloat(7.1)
fmt.Println(x) // 输出: 7.1
fmt.Println(x) // 输出: 7.1
类型断言
var i interface{} = "hello"
v := reflect.ValueOf(i)
if s, ok := v.Interface().(string); ok {
fmt.Println(s) // 输出: hello
}
v := reflect.ValueOf(i)
if s, ok := v.Interface().(string); ok {
fmt.Println(s) // 输出: hello
}
访问结构体字段
type MyStruct struct {
privateField int
}
s := MyStruct{privateField: 1}
v := reflect.ValueOf(s)
field := v.Elem().FieldByName("privateField")
fmt.Println("Private Field:", field.Int()) // 输出: Private Field: 1
privateField int
}
s := MyStruct{privateField: 1}
v := reflect.ValueOf(s)
field := v.Elem().FieldByName("privateField")
fmt.Println("Private Field:", field.Int()) // 输出: Private Field: 1
调用方法
type MyMethods struct{}
func (m *MyMethods) MyMethod() string {
return "Hello, World!"
}
obj := &MyMethods{}
method := reflect.ValueOf(obj).MethodByName("MyMethod")
result := method.Call(nil)
fmt.Println("Method Result:", result[0].Interface()) // 输出: Method Result: Hello, World!
func (m *MyMethods) MyMethod() string {
return "Hello, World!"
}
obj := &MyMethods{}
method := reflect.ValueOf(obj).MethodByName("MyMethod")
result := method.Call(nil)
fmt.Println("Method Result:", result[0].Interface()) // 输出: Method Result: Hello, World!
反射的性能和限制
性能开销:反射操作通常比直接代码执行要慢
类型限制值:反射不能用于非接口类型的变量
可访问性:私有字段和方法不能通过反射直接访问
除非使用reflect.Value的Unsage*方法
泛型
为一个函数添加能处理多种不同类型数据的能力
类型形参
类型实参
为什么不是反射?
使用麻烦
无编译时检查,容易出错
性能不理想
核心概念:type Slice[T int|float32|float64 ] []T
类型形参
类型形参列表
类型实参
类型约束
泛型类型
实例化
泛型接收器
错误写法
基础类型不能只有类型形参
type CommonType[T int|string|float32] T
类型约束被认为是表达式
type NewType [T * int][]T
解法
推荐:type NewType[T interface{*int}] []T
不推荐:type NewType[T *int,] []T
特殊的泛型类型
type Wow[T int | string] int
使用场景
泛型类型
泛型接收器
泛型函数
匿名函数不支持泛型,匿名结构体不支持泛型,不支持泛型方法
匿名函数支持使用别处已经定义好的泛型形参
动态判断泛型变量的类型
不可行
func (q *Queue[T]) Put(value T) {
value.(int) // 错误。泛型类型定义的变量不能使用类型断言
// 错误。不允许使用type switch 来判断 value 的具体类型
switch value.(type) {
case int:
// do something
case string:
// do something
default:
// do something
}
// ...
}
value.(int) // 错误。泛型类型定义的变量不能使用类型断言
// 错误。不允许使用type switch 来判断 value 的具体类型
switch value.(type) {
case int:
// do something
case string:
// do something
default:
// do something
}
// ...
}
可行
func (receiver Queue[T]) Put(value T) {
// Printf() 可输出变量value的类型(底层就是通过反射实现的)
fmt.Printf("%T", value)
// 通过反射可以动态获得变量value的类型从而分情况处理
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Int:
// do something
case reflect.String:
// do something
}
// ...
}
// Printf() 可输出变量value的类型(底层就是通过反射实现的)
fmt.Printf("%T", value)
// 通过反射可以动态获得变量value的类型从而分情况处理
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Int:
// do something
case reflect.String:
// do something
}
// ...
}
泛型中引入了反射,可行?
指定底层类型:~
后面的类型必须是基本类型,不能是接口
接口正式从方法集合转为类型集合
多行类型定义代表取它们的交集
type AllInt interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
type A interface { // 接口A代表的类型集是 AllInt 和 Uint 的交集
AllInt
Uint
}
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
type A interface { // 接口A代表的类型集是 AllInt 和 Uint 的交集
AllInt
Uint
}
空接口代表的是所有类型的集合,而不是只能用空接口做类型形参
Error
程序运行中可能会遇到各种错误,适当的错误处理可以确保程序在遇到问题时能够优雅的恢复和终止
文件找不到
网络连接失败
无效的输入数据
error:是一个接口类型,任何实现了error接口的类型都可以用于错误处理
返回错误
返回参数通常包含error
检查错误
if err != nil {
fmt.Println("Error:", err)
// 处理错误,例如记录日志、重试操作或返回错误给调用者
}
fmt.Println("Error:", err)
// 处理错误,例如记录日志、重试操作或返回错误给调用者
}
defer关闭资源
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
if err != nil {
return nil, err
}
defer file.Close()
错误包装:添加额外信息到原始错误中
return fmt.Errorf("operation failed: %w", err)
错误传播:多层上抛错误信息
Panic&Recover
panic:在遇到不可恢复的错误时,立即终止当前goroutine去执行,并开始进行栈展开
若当前是主goroutine的话,程序将退出
若当前是主goroutine的话,程序将退出
是一种错误处理机制,防止程序在错误状态下继续执行造成更严重的问题
程序趋于健壮、稳定
内建函数,携带一个错误参数
以一种可控的方式失败
用于处理不应该发生的错误
频繁使用导致栈展开,影响程序性能
发生场景
数据访问越界
类型断言失败
访问未初始化的指针
访问无效的map键
执行除以零操作
显式调用panic
std中的panic
sync.Map错误使用
json.Unmarshal解析无效的json数据
recover:捕获一个panic,恢复程序执行,只能在defer函数中调用
完整示例
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Panic occurred:", r)
}
}()
panic("something went wrong")
}
defer func() {
if r := recover(); r != nil {
fmt.Println("Panic occurred:", r)
}
}()
panic("something went wrong")
}
进阶部分
Goroutine
一种轻量级的线程,是并发的基本单元,由go运行时管理
协程的创建和调度由go运行时负责,使得协程更加轻量、高效、开销更小
轻量级线程:协程不需要像传统线程那样占用大量的系统资源,更快速、灵活的启动和执行
由go运行时调度:由go运行时自动管理,可以更加专注业务,而非传统线程那样需要手动管理线程的创建、调度和销毁
并发的基本单元:可以同时执行多个协程,由go运行时自动管理
轻松实现并发:可以轻松实现并发任务,基于其轻量的特性,可以创建数千甚至数百万协程
使用通道进行通信:使用通道进行协程间的协作和数据传递,这是一种安传的传递数据和同步操作的机制
更高效的资源利用:可以更高效的利用系统资源,使得程序具有更优的性能
基本操作
创建协程:go关键字声明
// 启动一个新的协程
go sayHello()
go sayHello()
协程通信:通过chan通信和传递数据
package main
import (
"fmt"
"time"
)
func sendMessage(ch chan string, message string) {
ch <- message // 发送消息到通道
}
func main() {
// 创建一个字符串通道
messageChannel := make(chan string)
// 启动一个协程发送消息
go sendMessage(messageChannel, "Hello from Goroutine!")
// 主线程从通道接收消息
receivedMessage := <-messageChannel
fmt.Println(receivedMessage)
}
import (
"fmt"
"time"
)
func sendMessage(ch chan string, message string) {
ch <- message // 发送消息到通道
}
func main() {
// 创建一个字符串通道
messageChannel := make(chan string)
// 启动一个协程发送消息
go sendMessage(messageChannel, "Hello from Goroutine!")
// 主线程从通道接收消息
receivedMessage := <-messageChannel
fmt.Println(receivedMessage)
}
协程同步:使用sync.WaitGroup实现协程的同步,等待一组协程执行完毕
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 协程完成时减少计数
fmt.Printf("Worker %d starting\n", id)
// 模拟一些工作
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 每启动一个协程增加计数
go worker(i, &wg) // 启动协程
}
wg.Wait() // 等待所有协程完成
fmt.Println("All workers done")
}
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 协程完成时减少计数
fmt.Printf("Worker %d starting\n", id)
// 模拟一些工作
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 每启动一个协程增加计数
go worker(i, &wg) // 启动协程
}
wg.Wait() // 等待所有协程完成
fmt.Println("All workers done")
}
错误处理:使用select语句去处理协程中的错误
package main
import (
"fmt"
"time"
)
func doSomething(ch chan string) {
// 模拟一些工作
time.Sleep(2 * time.Second)
ch <- "Task completed"
}
func main() {
ch := make(chan string)
go doSomething(ch)
select {
case result := <-ch:
fmt.Println(result)
case <-time.After(1 * time.Second):
fmt.Println("Timeout occurred")
}
}
import (
"fmt"
"time"
)
func doSomething(ch chan string) {
// 模拟一些工作
time.Sleep(2 * time.Second)
ch <- "Task completed"
}
func main() {
ch := make(chan string)
go doSomething(ch)
select {
case result := <-ch:
fmt.Println(result)
case <-time.After(1 * time.Second):
fmt.Println("Timeout occurred")
}
}
高级操作
协程池:chan+多个goroutine
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
const numJobs = 5
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 创建并启动协程池
var wg sync.WaitGroup
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
worker(i, jobs, results)
}(i)
}
// 提供任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 等待所有工作完成
go func() {
wg.Wait()
close(results)
}()
// 处理结果
for result := range results {
fmt.Printf("Result: %d\n", result)
}
}
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
const numJobs = 5
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 创建并启动协程池
var wg sync.WaitGroup
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
worker(i, jobs, results)
}(i)
}
// 提供任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 等待所有工作完成
go func() {
wg.Wait()
close(results)
}()
// 处理结果
for result := range results {
fmt.Printf("Result: %d\n", result)
}
}
基于chan做sema的实现方式会不会更好点?
超时控制:select+time.After
package main
import (
"fmt"
"time"
)
func longRunningTask(ch chan string) {
// 模拟一些长时间运行的任务
time.Sleep(3 * time.Second)
ch <- "Task completed"
}
func main() {
ch := make(chan string)
go longRunningTask(ch)
select {
case result := <-ch:
fmt.Println(result)
case <-time.After(2 * time.Second):
fmt.Println("Timeout occurred")
}
}
import (
"fmt"
"time"
)
func longRunningTask(ch chan string) {
// 模拟一些长时间运行的任务
time.Sleep(3 * time.Second)
ch <- "Task completed"
}
func main() {
ch := make(chan string)
go longRunningTask(ch)
select {
case result := <-ch:
fmt.Println(result)
case <-time.After(2 * time.Second):
fmt.Println("Timeout occurred")
}
}
协程的取消:带cancel函数的context
package main
import (
"fmt"
"context"
"time"
)
func doSomething(ctx context.Context, ch chan string) {
select {
case <-time.After(3 * time.Second):
ch <- "Task completed"
case <-ctx.Done():
ch <- "Task canceled"
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan string)
go doSomething(ctx, ch)
// 模拟在某个条件下取消任务
time.Sleep(2 * time.Second)
cancel()
result := <-ch
fmt.Println(result)
}
import (
"fmt"
"context"
"time"
)
func doSomething(ctx context.Context, ch chan string) {
select {
case <-time.After(3 * time.Second):
ch <- "Task completed"
case <-ctx.Done():
ch <- "Task canceled"
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan string)
go doSomething(ctx, ch)
// 模拟在某个条件下取消任务
time.Sleep(2 * time.Second)
cancel()
result := <-ch
fmt.Println(result)
}
实现原理
协程是并发编程的基本单元
核心概念
调度器
go运行时有一个调度器组件,负责协程的管理和执行
决定哪个协程可以运行、何时运行、在那个处理器上运行
当一个协程发生阻塞(io操作、睡眠)时,调度器会释放执行权限,切换另一个可运行的协程
M:N模型
M个调度器管理的协程对应N个系统线程,会将多个协程调度到低负载的系统线程上执行
栈
每个协程都有自己的栈,用于存储函数调用、局部变量等信息
go的协程栈是动态大小的,可以根据需要进行伸缩
调用约定
协程切换不需要保存整个栈,只需要保存少量上下文信息,使得协程切换更加高效
协程的管道和os的管道
协程的通道是用于协程之间轻量级通信的机制
并发编程中协调协程之间通信的机制,提供了一种无锁、安全传递数据的方式,帮助协程之间进行异步通信
通信方式同时支持同步和异步
由go标准库提供的,提供了基本的使用方法
通常设计为在单一进程内的轻量级通信,使得性能更加高效,适用于异步编程
os的管道是用于进程之间通信的机制
进程间的通信机制,是os提供的一种IPC方式,使得不同进程可以通过管道进行数据传输
基于文件描述符传递数据,一个进程写数据,另一个进程读数据,通常是阻塞的
操作系统提供的,通过系统调用来创建和管理
进程间通信的,涉及到扫内核空间和用户空间的切换,性能相对较低
协程通道实现方式
通过内置的channel类型来实现的,chan允许协程间安全的发送数据和接收数据
创建通道
ch := make(chan int)
发送消息
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Printf("Produced: %d\n", i)
time.Sleep(time.Second) // 模拟一些异步操作
}
close(ch) // 关闭通道,表示不再发送数据
}
for i := 0; i < 5; i++ {
ch <- i
fmt.Printf("Produced: %d\n", i)
time.Sleep(time.Second) // 模拟一些异步操作
}
close(ch) // 关闭通道,表示不再发送数据
}
接收消息
func consumer(ch <-chan int, done chan<- bool) {
for {
item, ok := <-ch
if !ok {
break // 通道已关闭,退出循环
}
fmt.Printf("Consumed: %d\n", item)
}
done <- true // 通知主协程消费完成
}
for {
item, ok := <-ch
if !ok {
break // 通道已关闭,退出循环
}
fmt.Printf("Consumed: %d\n", item)
}
done <- true // 通知主协程消费完成
}
信号量阻塞
func main() {
ch := make(chan int)
done := make(chan bool)
go producer(ch)
go consumer(ch, done)
// 阻塞等待消费完成
<-done
}
ch := make(chan int)
done := make(chan bool)
go producer(ch)
go consumer(ch, done)
// 阻塞等待消费完成
<-done
}
协程通道基本原理
用于协程之间安全通信的数据结构,基于CSP理论
CSP理论
通信顺序进程,是一种并发计算模型,通过进程间的通信来协调操作系统
提供一种清晰且形式化的方法,便于描述和理解并发系统的行为
强调通过通信而不是共享状态来协调进程,以减少一些典型的并发编程问题
核心概念
进程
进程是并发执行的基本单元,每个进程都有自己的局部状态和行为
进程之间通过通信进行协作,而不是共享状态
通信
进程之间的唯一交互是通过通信进行的,通信可以是异步和同步
进程通过发送和接收消息进行通信,这样可以安全的共享信息而不是共享状态
通道
通道是CSP中用于进程之间通信的抽象,是一个先进先出的消息队列
进程可以通过向通道发送消息来与其他进程通信,也可以通过从通道接收消息和获取信息
选择
CSP引入了选择机制,通过select语句,进程可以等待多个通信操作中的一个完成
select允许进程以非阻塞的方式等待多个通道操作,从而提供了更灵活的协作方式
并发和并行
CSP强调通过并发而不是并行来实现并发性,指进程之间的独立执行,而并行指在同一时刻执行多个操作
进程之间通过通信而不是共享状态来协调,从而避免了许多并发编程中的常见问题
顺序化组合
进程可以通过顺序化组合形成更复杂的系统,通过将多个进程按照某种方式连接起来,使其以一种协调的方式协同工作
基本操作
创建:使用make,指定通道中传输的数据类型
ch := make(chan int)
发送和接收
// 发送数据
ch <- 42
// 接收数据
value := <-ch
ch <- 42
// 接收数据
value := <-ch
阻塞和同步:都是阻塞的,等待对应的协程准备好
// 发送数据
ch <- 42
// 接收数据
value := <-ch
ch <- 42
// 接收数据
value := <-ch
通道的关闭:close关闭,表示不再发送数据;关闭后仍可以接收数据,但不能向通道发送数据
close(ch)
避免死锁:当所有协程都阻塞在通道的发送或接收操作时,可能会发生死锁;合理的关闭通道和使用select语句等方式,可以避免死锁
select {
case ch <- 42:
// 发送数据
case value := <-ch:
// 接收数据
}
// 使用通道关闭信号
close(ch)
case ch <- 42:
// 发送数据
case value := <-ch:
// 接收数据
}
// 使用通道关闭信号
close(ch)
有无缓冲通道
无缓冲通道可以保证发送和接收的同步性,发送者和接受者必须同时准备好
缓冲通道允许一定数量的数据在通道中等待,发送和接收操作可以异步进行
// 无缓冲通道
ch := make(chan int)
// 缓冲通道
ch := make(chan int, 1)
ch := make(chan int)
// 缓冲通道
ch := make(chan int, 1)
通道的选择
使用select语句可以同时等待多个通道操作,避免单一通道的阻塞
select {
case ch1 <- 42:
// 向通道1发送数据
case value := <-ch2:
// 从通道2接收数据
}
case ch1 <- 42:
// 向通道1发送数据
case value := <-ch2:
// 从通道2接收数据
}
协程通道的限制
阻塞和死锁:若发送者协程发送数据,但没有接受者来接收就会导致阻塞
func main() {
ch := make(chan int)
// 发送者
go func() {
ch <- 42
}()
// 没有接收者,导致死锁
// <-ch
}
ch := make(chan int)
// 发送者
go func() {
ch <- 42
}()
// 没有接收者,导致死锁
// <-ch
}
单向通道限制:单向通道只能发送或接收数据,不能同时进行;双向通道可以隐式转换为单向通道
func main() {
ch := make(chan int)
sendOnly := make(chan<- int)
receiveOnly := make(<-chan int)
sendOnly = ch
receiveOnly = ch
}
ch := make(chan int)
sendOnly := make(chan<- int)
receiveOnly := make(<-chan int)
sendOnly = ch
receiveOnly = ch
}
关闭已关闭的通道:关闭已经关闭的通道会panic,应该在关闭之前检查通道的状态
func main() {
ch := make(chan int)
close(ch)
// 尝试关闭已关闭的通道,导致panic
// close(ch)
}
ch := make(chan int)
close(ch)
// 尝试关闭已关闭的通道,导致panic
// close(ch)
}
缓冲通道的容量限制:固定容量,满了会阻塞发送者;当通道为空时,会阻塞接受者
func main() {
ch := make(chan int, 1)
// 发送者,通道已满,导致阻塞
ch <- 42
// 接收者,通道为空,导致阻塞
// <-ch
}
ch := make(chan int, 1)
// 发送者,通道已满,导致阻塞
ch <- 42
// 接收者,通道为空,导致阻塞
// <-ch
}
无缓冲通道的同步性:发送和接收都是同步的,发送者和接收者都会被阻塞,直到另一方准备好;该方式在某些情况下会有性能问题
func main() {
ch := make(chan int)
// 发送者和接收者都会被阻塞,直到另一方准备好
go func() {
ch <- 42
}()
// ...
}
ch := make(chan int)
// 发送者和接收者都会被阻塞,直到另一方准备好
go func() {
ch <- 42
}()
// ...
}
协程的限制
调度器限制:go运行时调度器使用一定的策略来在少量的操作系统傻姑娘调度大量的go协程
栈大小限制:固定大小的栈,通常是几kb;如果协程的栈空间用尽,程序会发生栈溢出
内存使用限制:受限于系统的物理内存,大量的go协程可能会导致内存占用过高
并发数限制:取决于系统的资源和go运行时的配置;过多的并发可能会导致系统资源耗尽
无法跨CPU核执行:绑定一个操作系统线程,无法跨越多个CPU核执行
无法主动释放协程:由go运行时管理,不能手动终止或释放
slice
切片数据结构
type slice struct {
array unsafe.Pointer
len int
cap int
}
array unsafe.Pointer
len int
cap int
}
创建数组
创建方式1
a := []int{1, 2, 3, 4, 5}
fmt.Println(len(a), cap(a)) // 5, 5
fmt.Println(len(a), cap(a)) // 5, 5
创建方式2
c := *new([]int)
fmt.Println(len(c), cap(c)) // 0, 0
实现原理
// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function.
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.Size_, typ, true)
}
// compiler (both frontend and SSA backend) knows the signature
// of this function.
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.Size_, typ, true)
}
创建切片
创建方式1
b := a[1:3:4] // [low:high:max]
fmt.Println(len(b), cap(b), b) // 2, 3, [2 3]
fmt.Println(len(b), cap(b), b) // 2, 3, [2 3]
max表示最大索引值
容量计算
容量上限
避免内存共享风险
创建方式2
d := make([]int, 5, 5)
fmt.Println(len(d), cap(d)) // 5, 5
fmt.Println(len(d), cap(d)) // 5, 5
实现原理:计算所需内存大小,超出限制则panic,反之创建底层数据
makeslice函数
扩容切片
append追加元素时,若cap>=len+num时,则需扩容
growslice
nextslicecap:小于256采用2倍扩容,超过则采用1.25倍扩容
roundupsize:字节对齐(使CPU读取字节更加高效)
拷贝切片
slicecopy:若目标切片小于源切片,就只拷贝目标长度的内容,指的是len
常见坑法
引用类型
底层数组是共享存储的
a := []int{1, 2, 3, 4, 5}
b := a[1:2]
fmt.Println(a, b)
a[1] = 6
fmt.Println(a, b)
b := a[1:2]
fmt.Println(a, b)
a[1] = 6
fmt.Println(a, b)
参数传递
传递的是底层数组的指针;除非出发切片扩容,否则修改的都是同一个底层数组
func change(b []int) {
b[1] = 6
}
func main() {
a := []int{1, 2, 3, 4, 5}
fmt.Println(a)
change(a)
fmt.Println(a)
}
b[1] = 6
}
func main() {
a := []int{1, 2, 3, 4, 5}
fmt.Println(a)
change(a)
fmt.Println(a)
}
遍历切片
v是切片的值的一个拷贝,修改不会影响到切片,但是直接修改切片就有可能影响到
func main() {
a := []int{1, 2, 3, 4, 5}
fmt.Println(a)
for _, v := range a {
a = append(a, v)
}
fmt.Println(a)
}
a := []int{1, 2, 3, 4, 5}
fmt.Println(a)
for _, v := range a {
a = append(a, v)
}
fmt.Println(a)
}
该处的for循环次数从一开始就已经确定
map
一个存储了KV键值对的数据结构,其中K是唯一的,不同的K可以对应相同的V
Hash算法:对同一个关键字进行哈希计算,每次计算都是相同的值
Hash碰撞
链表法(拉链法):把发生冲突的key的value组成一个链表
开放地址法:通过一定的规律,在数组的后面挑选空位,用来放置新的key
map数据结构图
hmap
hmap中的buckets存储所有的bucket
通过hash决定一个key和value存储在那个bucket
bmap
所有的key放一块,所有的value放一块
内存对齐减少内存浪费,加快读取效率
采用链地址法,每个bucket下有一个overflow属性指向其他的bucket
定位方式:hash的后B位得到桶的位置,前八位得到桶中的位置,反之则在溢出桶中操作
扩容:负载因子小于进行等值扩容,大于6.5时进行二倍扩容
先将bucket挂到oldbucket上,再将bucket指向新的bucket地址
渐进式扩容:每次插入、修改、删除操作就迁移两个桶
都迁移完成后oldbucket变成nil
channel
结构体
内存管理
原理知识
逃逸分析
GMP模型
垃圾回收
协程泄漏
0 条评论
下一页