go的context

context

包(context)类型是包装了channel通信来关联goroutine服务之间的控制机制,支持树状的上级控制一个或者多个下级,不支持反向控制和平级控制,同理参数的共享是树状的传递流动方向,也可以用来管理有超时依赖的函数,以下代码实例里面,存在forever for循环的才算服务。

参数共享特性

  • context的参数存储不是map,是struct的key和val两个属性,对外暴露操作接口
  • context的参数由父goroutine初始化,子goroutine调用
  • 父context会嵌入子context,同名参数相当于有多个版本
  • 子context获取参数会按照embed的向上查找机制获得多层父级context的参数,同名参数优先使用子context的参数版本
  • 子context初始化非同名参数,相当于增加新参数
  • 子context初始化同名参数,相当于增加一个参数版本
  • context传递的参数注意选择必要的,简单的,size小的
  • context因为只读所以是gorountine安全的,可以多个子gorountine服务共享同一个context的父控制

控制特性

  • 父gorountine服务通过context控制子gorountine服务,由struct里面的空channel实现,父gorountine服务close空channel,子gorountine服务的select监听并可能触发关闭事件,结束生命周期
  • 父gorountine服务达到某种状态,传递信号,结束子gorountine服务
  • 父gorountine服务初始化启动子gorountine服务时,传入截止时刻或者超时时间
  • 父gorountine结束,则所有的子gorountine服务结束
  • 以上三条规则共同决定一个gorountine服务的生命周期

使用

1
2
3
4
5
6
type Context interface {
Done() <-chan struct{} //控制生命周期的空channel
Err() error //获取错误码
Deadline() (deadline time.Time, ok bool) //获取截止时间
Value(key interface{}) interface{} //传递参数
}

子goroutine服务的关联的context是通过WithCancel, WithDeadline, WithTimeout, WithValue复制父goroutine服务的内容,派生出新的context。CancelFunc可以取消所有的后代,移除父goroutine服务在子goroutine服务的关联,并停止所有关联timer。

1
2
3
4
5
6
7
8
9
10
11
package main
import "context"
func tree() {
ctx1 := context.Background()
ctx2, _ := context.WithCancel(ctx1)
ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
ctx6 := context.WithValue(ctx5, "userID", 12)
}

以上代码的Context链下图:
ctx
3秒超时:ctx4超时退出
ctx
5秒超时:ctx3超时退出,导致子节点ctx5和ctx6都退出
ctx
这里只是形象的表示了多级context之间形成的控制关系,实际业务里面ctx5,ctx6所在的goroutine如果在执行业务是不会立即退出的,只有当goroutine在等待IO事件等待数据的时候才会由select响应结束的case。

Background和TODO
都是返回一样的无控制条件无参数的empty Context,background在主服务里面通常作为匿名参数生成可控的context变量;todo可能会用于程序兼容处理,当空桩,做空调用

WithCancel
传入父Context,返回可控子Context,并且返回结束调用子context的goroutine的控制函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
package main
import "context"
func main() {
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for { //for循环是goroutine 函数作为服务的标志
select {
case <-ctx.Done():
return
case dst <- n: //这里堵塞有可能会成为函数的超时依赖
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() //一定要手动加取消操作
for n := range gen(ctx) {
println(n)
if n == 5 {
break
}
}
}

WithDeadline
传入父Context和截止期限,返回包含截止期限的可控子Context,并且返回结束调用子context的goroutine的控制函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
package main
import "context"
import "time"
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
select {
case <-time.After(1 * time.Second):
println("overslept")
case <-ctx.Done():
println(ctx.Err())
}
}

WithTimeout
传入父Context和超时时间,返回包含超时时间的可控子Context,并且返回结束调用子context的goroutine的控制函数

1
//func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue
传入父Context和参数key-val,返回包含参数的子Context,如果父context是background,那么子context不可控,否则可控

  • 在服务里面context.Background(),不适合作为有变量接收的WithValue的参数,可以是ctx := context.WithCancel(context.WithValue(context.Background(), k, “Go”))
  • context的key是包内的全局变量,不可导出,避免冲突
  • context的key的数据类型必须支持==和!=,类似于map的key,包括bool,number,string,pointer,channel,interface和包含上述类型的array,struct。不支持slice,map,function或者包含它们的类型。
  • 存储的数据最好是type-safe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//func WithValue(parent Context, key, val interface{}) Context
package main
import "context"
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
println("found value:", v)
return
}
println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}

go vet tool可以检查CancelFuncs的使用情况。context参数不要包裹在struct等一些复杂的数据结构里面,context参数位置放在函数的第一个变量,context参数不要传递nil。

问题

如何实现平级控制,一个子goroutine出现问题同时取消共享父控制的其他子goroutine,参见golang.org/x/sync/errgroup

To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables’ static type should be a pointer or interface. 这是什么意思?