ctx
字面意思上下文
,是golang中特有的一种语法,几乎每一个程序中都会通篇传递着一个ctx
。而一些框架又对其进行二次封装,诸如Gin框架中的c *gin.Context
。因此此次进行ctx
的学习并记录。
这是ctx
的接口部分,其提供了一个接口及许多函数、结构体(如图)。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
其通过context.Background、context.TODO、context.WithDeadline 和 context.WithValue
来返回实现这个接口的结构体。
goroutine树和ctx是上下传递的,会从顶层一步步传往底层。详见
多个goroutine运行,如何控制其结束?
最Low的办法自然是造一个bool
全局变量,多个goroutine中检测其bool
值,若改变,则停止。
但这样显然是不合理的,于是便可以通过select
+chan
来进行控制,即在goroutine中放一个select
,其中通过chan
到某个值,就停止。
func main() {
stop := make(chan bool)
go func() {
for {
select {
case <-stop: // 收到了停滞信号
fmt.Println("监控退出,停止了...")
return
default:
fmt.Println("goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
stop<- true
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
但这样也有很大的局限性,比如我的goroutine中又新建了goroutine,这种无法在程序开始前写到的地方,该如何接收?
因此便可以用到ctx
来进行控制。
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("监控退出,停止了...")
return
default:
fmt.Println("goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}(ctx)
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
两个方法看似一样,只是将手动向<-stop
收发,换成了使用cancel()
和<-ctx.Done()
。根据源码可以看出,其底层原理也是通过chan
在进行通信,但是由于ctx
及goroutine树
,所以可以解决goroutine中新建goroutine的问题,从而更优雅的关闭goroutine。
通过ctx.Value("key")
来读取,通过ctx = context.WithValue(ctx, "key", value)
来设置。
但仅建议传递内置类型的数据。不建议传递重要数据,比如传递 签名、trace_id这类值。
即通过context.WithDeadline(ctx, time.Now().Add(10*time.Second))
设置这个ctx的关闭时间。
通过设置deadline
,可以防止一个请求超时导致服务器问题。
需要注意的是不同ctx中deadline
不一致导致问题。
(一次我使用context.Context
和*gin.Context
时遇到了莫名其妙的超时,就是因为这个不一致)
1、不要把 Context 放在结构体中,要以参数的方式传递。
2、以 Context 作为参数的函数方法,应该把 Context 作为第一个参数,放在第一位。
3、给一个函数方法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使用 context.TODO。
4、Context 的 Value 相关方法应该传递必须的数据,不要什么数据都使用这个传递。
5、Context 是线程安全的,可以放心的在多个 goroutine 中传递。