go 语言进阶什么是 context context 是协程的上下文,在 go 1.7 版本中引入,用于传递跟踪、取消信
什么是 context
context
是协程的上下文,在 go 1.7
版本中引入,用于传递跟踪、取消信号和超时信息
context
最佳实践的:
- 在函数的签名中传递
context
,不推荐在结构体中使用context
context
作为函数的第一个参数传递- 不要在内层函数创建
context
,如果在内层函数中创建context
,会导致context
范围使用不够明确,容易造成context
误用,造成内存泄露- 应该在
fn1
函数中创建context
,然后传递给fn2
函数,fn2
函数再传递给fn3
函数
fn1() -> fn2() -> fn3()
- 应该在
- 及时取消
context
,可以避免资源浪费func main() { parentCtx := context.Background() ctx, cancel := context.WithTimeout(parentCtx, time.Second*3) // 设置 3s 超时 defer cancel() if err := longRunningFunction(ctx, "arg1", 2); err != nil { fmt.Println("longRunningFunction error: ", err) // longRunningFunction error: context deadline exceeded cancel() } } func longRunningFunction(ctx context.Context, arg1 string, arg2 int) error { select { case <-time.After(time.Second * 5): // 模拟耗时操作, 5s case <-ctx.Done(): // 超时或者取消 return ctx.Err() } return nil }
- 不要在
context
中存储大量数据,只存储一些必要的数据,比如userId
、requireId
等,不要传递敏感信息 - 不要滥用
context
,context
只在当前请求范围内中使用,不要在全局中使用,如果需要在多个请求共享数据,应该用其它方式
panic 和 recover
panic
和 recover
使用的细节:
recover
必须在defer
声明的匿名函数中执行- 如果不使用
defer
,recover
是在panic
之前运行的,就无法捕获panic
- 如果不使用
recover
与函数调用在同一个协程才能捕获到当前函数的panic
- 当前
goroutine
中的panic
会被defer
中的panic
覆盖 - 多个
defer
语句中panic
的执行顺序- 如果没有
recover
语句,会依此输出b
、a
、panic: panic main
外加panic
的堆栈信息
func fn() { defer func() { fmt.Println("a") // 再输出 a panic("panic a") }() defer func() { fmt.Println("b") // 先输出 b panic("panic b") }() panic("panic main") // 最后抛出 panic main,程序结束 }
- 如果有
recover
语句,会依此输出b
、a
、recover: panic a
func fn() { defer func() { if err := recover(); err != nil { fmt.Println("recover: ", err) // recover: panic a } }() defer func() { fmt.Println("a") // 再输出 a panic("panic a") }() defer func() { fmt.Println("b") // 先输出 b panic("panic b") }() panic("panic main") }
- 如果没有
- 多个调用链捕获
panic
,会优先被当前协程中的recover
捕获
channel
channel
的数据结构:
type hchan struct {
// 当前队列中元素的个数,当我们向 channel 发送数据时,qcount 会加 1,当我们从 channel 接收数据时,qcount 会减 1
qcount uint
// 如果我们在创建 channel 时制定了缓冲区的大小,嘛呢 dataqsiz 就等于制定的大小,否则 dataqsiz 为 0,表示该channel 没有缓冲区
dataqsiz uint
// buf 字段是一个 unsafe.Pointer 类型的指针,指向缓冲区的起始地址。如果该 channel 没有缓冲区,则 buf 为 nil
buf unsafe.Pointer
// 表示缓冲区中每个元素的大小,当我们创建 channel 时,go 会根据元素的类型计算出 elemsize 的值
elemsize uint16
// channel 是否已经关闭,当我们通过 close 函数关闭一个 channel 时,go 会将 closed 字段设置为 1
closed uint32
// 表示下一次接收元素的位置,当我们从 channel 中接收数据时,go 会从缓冲区中 recvx 索引的位置读取数据,并将 recvx 加 1
recvx uint
// 表示下一次发送元素的位置,在 channel 的发送操作中,如果缓冲区未满,则会将数据写入到 sendx 指向的位置,并将 sendx 加 1
// 如果缓冲区已满,则发送操作会被阻塞,直到有足够的空间可用
sendx unit
// 等待接收数据的 goroutine 队列,用于存储等待从 channel 中读取数据的 goroutine
// 当 channel 中没有数据可读时,接收者 goroutine 会进入 recvq 等待队列中等待数据的到来
// 当发送者 goroutine 写入数据后,会将 recvq 等待队列中的接收者 goroutine 唤醒,并进行读取操作
// 在进行读取操作时,会先检查 recvq 等待队列是否为空,如果不为空,则会将队列中的第一个 goroutine 唤醒进行读取操作
// 同时,由于 recvq 等待队列时一个 FIFO 队列,因此等待时间最场的 goroutine 会排在队列的最前面,最先被唤醒进行读取操作
recvq waitq
// 等待发送数据的 goroutine 队列。sendq 字段是一个指向 waitq 结构体的指针,waitq 是一个用于等待队列的结构体
// wait 中包含了一个指向等待队列中第一个协程的指针和一个指向等待队列中最后一个协程的指针
// 当一个协程向一个 channel 中发送数据时,如果该 channel 中没有足够的缓冲区来存储数据,那么发送操作将会被阻塞
// 直到有另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据,当一个协程被阻塞在发送操作时
// 它将会被加入到 sendq 队列中,等待另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据
sendq waitq
// channel 的读写锁,确保多个 goroutine 同时访问时的并发安全,保证读写操作的原子性和互斥性
// 当一个 goroutine 想要对 channel 进行读写操作室,首先需要获取 lock 锁。如果当前 lock 已经被其他 goroutine 占用
// 则该 goroutine 被阻塞,直到 lock 被释放,一旦该 goroutine 获取到 lock,就可以进行读写操作,并且在操作完成后释放 lock
// 以便其他 goroutine 可以访问到 channel
lock mutex
}
channel
需要注意的:
- 一个只读的
channel
或一个只写的channel
都会导致编译不通过 - 双向
channel
可以隐式转换成单向的channel
,反之不行 - 向一个已经关闭的
channel
发送数据会导致panic
,从一个已经关闭的channel
读取数据会返回channel
中剩余的数据,如果channel
中没有数据,会返回channel
中元素的零值func main() { ch := make(chan int, 1) ch <- 1 a, ok := <-ch // 1, true fmt.Println(a, ok) close(ch) c, ok := <-ch fmt.Println(c, ok) // 0, false => 表示 channel 已经被关闭了 }
channel
的使用误区:
- 多次关闭
channel
会导致panic
channel
中传输的数据大小有限制func main() { arr := [4096]string{} ch := make(chan [4096]string) // channel element type too large (>64kB) ch <- arr time.Sleep(time.Second * 2) }
- 为什么
go
要对channel
的大小进行限制?channel
在传递数据时,会将数据进行拷贝,从一个channel
到另一个channel
至少会被复制一次,传递的值在某个channel
中停留过,那个这个值会被复制两次,一次发生在发送协程向缓冲队列推入值时,另一次会发生在接收协程从缓冲队列中取出值时,为了避免过大的复制成本,go
限制了channel
中传输的数据大小
- 为什么
优雅的退出
监听退出信号:
os.SIGHUP
:值1
,终端控制进程结束(终端连接断开)os.SIGINT
:值2
,用户发送INTR
字符(Ctrl + C
) 触发os.SIGTERM
:值15
,结束程序(可以被捕获,阻塞或忽略)os.SIGQUIT
:值3
,用户发送QUIT
字符(Ctrl + /
)os.SIGKILL
:值9
,无条件结束程序(不能被捕获,阻塞或忽略),一般不用监听
func fn(){
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
s := <-signals
log.Println("receive system signal: ", s)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 需要实现一个 shutdown 方法
err := server.Shutdown(ctx)
if err != nil {
log.Println("server shutdown error: ", err)
}
}
优雅关闭服务流程:
- 捕获终止信号
- 停止接收新的请求或连接
- 等待已有的请求或连接处理完毕
- 释放所有占用的资源
- 退出程序
转载自:https://juejin.cn/post/7405250711284596774