🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] ## Context 对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用`WithCancel`、`WithDeadline`、`WithTimeout`或`WithValue`创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。 ### 阅读延申 https://mp.weixin.qq.com/s/GldXbE9z-2FkWkEs_eE1pg ### Context接口构成 ~~~ type Context interface { // 返回 context 是否设置了超时时间以及超时的时间点 // 如果没有设置超时,那么 ok 的值返回 false // 每次调用都会返回相同的结果 Deadline() (deadline time.Time, ok bool) // 如果 context 被取消,这里会返回一个被关闭的 channel // 如果是一个不会被取消的 context,那么这里会返回 nil // 每次调用都会返回相同的结果 Done() <-chan struct{} // 返回 done() 的原因 // 如果 Done() 对应的通道还没有关闭,这里返回 nil // 如果通道关闭了,这里会返回一个非 nil 的值: // - 若果是被取消掉的,那么这里返回 Canceled 错误 // - 如果是超时了,那么这里返回 DeadlineExceeded 错误 // 一旦被赋予了一个非 nil 的值之后,每次调用都会返回相同的结果 Err() error // 获取 context 中保存的 key 对应的 value,如果不存在则返回 nil // 每次调用都会返回相同的结果 Value(key interface{}) interface{} } ~~~ * `Deadline`方法需要返回当前`Context`被取消的时间,也就是完成工作的截止时间(deadline); * `Done`方法需要返回一个`Channel`,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用`Done`方法会返回同一个Channel; * `Err`方法会返回当前`Context`结束的原因,它只会在`Done`返回的Channel被关闭时才会返回非空的值; * 如果当前`Context`被取消就会返回`Canceled`错误; * 如果当前`Context`超时就会返回`DeadlineExceeded`错误; * `Value`方法会从`Context`中返回键对应的值,对于同一个上下文来说,多次调用`Value`并传入相同的`Key`会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据; **其主要的应用 :** 1:上下文控制, 2:多个 goroutine 之间的数据交互等, 3:超时控制:到某个时间点超时,过多久超时。 ### Context 类型的结构体 #### emptyCtx emptyCtx 不是一个结构体,它只是 int 类型的一个别名,实现的 Context 的四个方法都是返回 nil 或者默认值: ~~~ type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil } ~~~ 这意味着 emptyCtx 永远不能被取消,没有 deadline,并且也不会保存任何值。它是一个私有类型,没有提供相关的导出方法,但是却被包装成了两个可以被导出的 ctx,用作顶层 Context: ~~~ var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo } ~~~ ### Background()和TODO() * background 通常可以用于 main 函数、初始化和测试,作为请求上下文的最顶层(根节点)。 * todo 当你不知道需要传入什么样的 context 的时候,就可以使用它,它可以随时被替换成其他类型的 context。 实际上这俩完全没有任何区别,但是通过不同的命名 `background`和`todo`本质上都是`emptyCtx`结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。 ~~~ 创建父节点 context.Background() context.TODO() ~~~ ### With函数 #### WithCancel ~~~go func WithCancel(parent Context) (ctx Context, cancel CancelFunc) ~~~ 返回:父节点的副本,cancle的函数 #### WithDeadline ~~~go func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) ~~~ 入参:deadline 的截至时间点,代表到了这个时间就会自动取消。 eg: ~~~ ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Minute)) fmt.Println(ctx.Deadline()) //return:2022-07-18 11:39:07.3568866 +0800 CST m=+60.002051901 true ~~~ #### WithTimeout ~~~go func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) ~~~ 入参:持续时间,代表 ctx 会在多长时间之后自动取消 eg:入参是5s,那么就5秒结束 #### WithValue ~~~go func WithValue(parent Context, key, val interface{}) Context ~~~ 入参:父节点副本,key,val,设置值使用 eg: ~~~ type MySting string //定义类型 func main() { var s MySting var s1 MySting s = "log" s1 = "log1" ctx := context.WithValue(context.Background(), s, 99) fmt.Println(ctx.Value(s)) ctx1 := context.WithValue(ctx, s1, 100) fmt.Println(ctx1 .Value(s1)) } // return : 99 100 ~~~ > 一个 ctx 只能保存一对 kv,那么如果我们想要在 ctx 中保存多个 kv 键值对该怎么办? 只需要多次调用 `WithValue()` 函数,每次塞进去一对 kv 即可,在取值的时候,`Value()` 方法会自动地从整个 ctx 的树形结构中递归地往上层查找。所以,我们可以通过子 ctx 找到 父 ctx 维护的 kv,但是反过来是不可以的,这一点在使用的过程中需要注意。 举个例子,如下图所示,假设当前在 Context3,我们想要查 key1 的值,发现当前 ctx 维护的 key 不是 key1,那么会从它的父节点也就是 Context2 去找,还没找到,便继续往上层去找,发现 Context1 维护的 key 就是我们想要的,那么 `Value` 方法便会返回它对应的 val1。 ![](https://img.kancloud.cn/c3/96/c3969901bce79c032e0576d5a48b839f_1080x191.png) ## context 使用中的注意事项 1、context 携带的 kv 是向上查找的,如果当前节点查不到对应的 key,那么会继续从其父节点中查找; 2、context 的取消操作是向下蔓延的,如果当前节点取消,那么它的子节点(cancelCtx)也会被取消; 3、使用带有超时的 timerCtx,如果能提前取消,那么最好手动提前取消,从而可以快速释放资源,同时需要注意的是 context 的取消操作针对的只是 context,如果还涉及到一些其他的操作,例如和数据库通信、文件读写等,这些也需要我们手动取消。 以下几点是使用 context 的一些约定俗成的建议: 1、不要将 context 塞到结构体里面,相反的它应该作为函数的第一个参数,并且统一命名成 ctx; 2、不要传入一个 nil context,如果不知道传啥,可以使用 context.TODO() 传入一个 emptyCtx; 3、context 中存储的应该是贯穿整个生命周期的数据,例如用户的 session、cookie 等,不要把本应该作为函数参数的数据放进 context 中; 4、**key 的类型最好不要是字符串类型或者其它内建类型**,否则容易在包之间使用 Context 时候产生冲突。使用 `WithValue` 时,**key 的类型最好是自己定义的类型**; 5、**context 是天然并发安全的**,不需要担心多个 goroutine 对它的并发操作。 ### 典型应用 #### 数据传递 ~~~ const KEY_LOG = "LOG_ID" const KEY_USER_ID = "USER_ID" func TestWithValue(t *testing.T) { // 通过 WithValue() 生成一个保存 key-value 键值对的 ctx ctx := context.WithValue(context.Background(), KEY_LOG, "2021082900001") // 链式存入第二个 key ctx = context.WithValue(ctx, KEY_USER_ID, "112233") logId := GetLogID(ctx) t.Log(logId) } func GetLogID(ctx context.Context) string { // 通过 Value() 方法查找 if logId := ctx.Value(KEY_LOG); logId != nil { return logId.(string) } return "" } ~~~ #### 取消协程执行 通过检查 Context 的 `Done` 方法我们可以判断它是否被 cancel 了。同时 context 还提供了两个带有超时功能的方法,分别是 `WithTimeout` 和 `WithDeadline` ,它们本质上是一样的,只不过前者的入参是超时时间,后者的入参是截至的时间,通过这两个方法生成的 ctx,都能够实现在时间到了之后,自动执行 `cancel` 方法,当然我们也可以选择(最好)在超时时间到来之前手动调用 `cancel` 。在很多微服务调用的实现场景,都是通过它们来实现远程调用的超时控制的。 ~~~ func TestWithCancel(t *testing.T) { ctx, cancel := context.WithCancel(ctx) // WithTimeout 可以实现超时自动调用 Cancel() // ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) go TakeTasks(ctx, "c1") // ctx2 是 ctx 的子 context,当 ctx 被取消之后,ctx2 也会被取消 ctx2, _ := context.WithCancel(ctx) go TakeTasks(ctx2, "c2") time.Sleep(500 * time.Millisecond) // 也可以手动提前取消 cancel() time.Sleep(100 * time.Millisecond) } func cancelled(ctx context.Context) bool { select { case <-ctx.Done(): fmt.Println("finish taking tasks!") return true default: fmt.Println("continue!") return false } } func TakeTasks(ctx context.Context, flag string) { for { if cancelled(ctx) { break } fmt.Printf("%s taking tasks!\n", flag) time.Sleep(100 * time.Millisecond) } } ~~~ context 的取消操作是一层一层往下传递的。也就是说在调用 `cancel()`  之后,对应的 ctx 会先标记自己已经被取消,然后它会向它的所有子 ctx 传达取消信号,通知它们也应该被取消掉了。