一、出现原因以及作用
在go语言并发编程中,用一个goroutine来处理一个任务,而它又可能再去创建多个goroutine来负责不同子任务。 这些场景中往往需要在API边界之间以及过程之间传递截止时间、取消信号或其他请求相关的数据,这时候就可以使用context!
二、Context主要内容
- Context接口
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }
- Context的四种实现
-
emptyCtx:本质是一个int,简单的返回nil、false等,Background和TODO这两个函数内部都会创建emptyCtx。其中Background主要用于初始化时获得一个ctx根节点。TODO官方文档的解释是在本来应该使用外层传递的ctx,而外层却没有传递的地方使用。
type emptyCtx int var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
-
cancelCtx:这是一种可取消的Context,done用于获取Context的取消通知,children用于存储以当前节点为根节点的所有可取消的Context,以便在根节点取消时,可以把它的子节点一起取消,err用于存储取消时指定的错误信息,mu用来保证线程并发安全。WithCancel函数可以把一个Context包装为cancelCtx,并提供一个取消函数。调用它可以取消对应的Context。
type cancelCtx struct { Context mu sync.Mutex // protects following fields done atomic.Value // of chan struct{}, created lazily, closed by first cancel call children map[canceler]struct{} // set to nil by the first cancel call err error // set to non-nil by the first cancel call } func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } }
-
timerCtx:在cancelCtx基础上封装了一个定时器和一个截止时间,这样既可以根据需要主动取消,也可以在到达deadline时,通过timer来触发取消动作,timer也会由cancelCtx结构体的mu来保护并发安全。WithTimeout函数和WithDeadline函数本质上都是调用了WithDeadline,不同是WithTimeout取了一个时间段,而WithDeadline是需要指定一个时间点。
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time } func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { ... return c, func() { c.cancel(true, Canceled) } }
-
valueCtx:它用来支持键值对打包,WithValue函数可以给Context附加一个键值对信息,这样就可以通过context传递数据了。
type valueCtx struct { Context key, val any } func WithValue(parent Context, key, val any) Context { if parent == nil { panic("cannot create context from nil parent") } if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val} }
-
三、使用注意项
-
cancelCtx的取消流程
- 如果ctx2先取消,只会影响以它为根节点的context
- 如果ctx1先取消,就可以根据children中的记录,把ctx1子节点中可以取消的context全部cancel掉 (包括ctx2)
由此可知,cancel流程是根据父子结点的顺序执行的
-
valueCtx取值问题
当我们使用的相同key值,keyB会覆盖keyA的valuectx := context.Background() var keyA = "keyA" ctxA := context.WithValue(ctx, keyA, "A") var keyB = "keyA" ctxb := context.WithValue(ctxA, keyB, "B") fmt.Println("keyA", ctxb.Value(keyA)) //B fmt.Println("keyB", ctxb.Value(keyB)) //B
-
其原因是,当前context中的key会比较是否等于要查找的key,如果不等于才会向父结点继续寻找。
func (c *valueCtx) Value(key any) any { if c.key == key { return c.val } return value(c.Context, key) }
-
为了解决这种key值相等的问题,可以将key类型设置为不相同解决
type keyTypeA string type keyTypeB string var keyA keyTypeA = "keyA" var keyB keyTypeB = "keyB"
-
-
Context本身是为了不可改变(immutable)的模式设计的,因此不要尝试修改ctx里保存的值