1. 简介
go 1.7 开始引入context(上下文),准确地说是goroutine 的上下文。主要在goroutine 间传递上下文消息,包括了取消信号, 超时时间,截止时间和k-v 等。
在web程序中,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的 goroutine去访问后端资源,比如数据库、RPC服务等,它们需要访问一些共享的资源,比如用户身份信息、认证token、请求截止时间等 这时候可以通过Context,来跟踪这些goroutine,并且通过Context来控制它们, 这就是Go语言为我们提供的Context,中文可以称之为“上下文”。
2. 接口定义
主要包括了如下两个接口定义
- Context
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
-
Deadline
方法是获取设置的截止时间的意思,第一个返回值是截止时间,到了这个时间点,Context会自动发起取消请求; 第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。 -
Done
方法返回一个只读的chan,类型为struct{},在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求, 我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。之后,Err 方法会返回一个错误,告知为什么 Context 被取消 -
Err
方法返回取消的错误原因,因为什么Context被取消,例如 被取消或者超时 -
Value
方法获取该Context上绑定的值,是一个键值对,通过一个Key才可以获取对应的值
-
-
canceler
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} }
实现了canceler 接口的context说明是可取消的,在context 包中有两个类型实现了该接口,分别是:
*cancelCtx
和*timerCtx
3. 接口实现类型
官方提供了如下四种的 context 类型,分别是 emptyCtx,cancelCtx,timerCtx,valueCtx。
3.1 emptyCtx
实现了Context接口,但都是直接 return 默认值,没有具体功能代码, 底层类型是int
, 同时实现了Stringer
接口(String方法)
type emptyCtx int
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
emptyCtx一般用作最初始的 context,作为父 context 使用, 官方定义了两个可导出的empptCty,通过函数Background()
和TODO()
返回
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
3.2 cancelCtx
实现了Context
和canceler
接口, 结构体定义如下
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
}
-
接口实现
cancelCtx组合了匿名Context 字段,所以也是一个Context, 同时它又实现了
canceler
接口,
其中对于Context, 重写了Done()
,Value()
和Err()
方法,其他的继承父类的
具体代码如下:// `Done()` 方法 采用`懒汉式`方式, 当调用该方法时,`c.done`才会存储一个新创建的一个只读 `channel d`,并返回 func (c *cancelCtx) Done() <-chan struct{} { d := c.done.Load() if d != nil { return d.(chan struct{}) } c.mu.Lock() defer c.mu.Unlock() d = c.done.Load() if d == nil { d = make(chan struct{}) c.done.Store(d) } return d.(chan struct{}) } func (c *cancelCtx) Value(key any) any { if key == &cancelCtxKey { return c } return value(c.Context, key) } func value(c Context, key any) any { for { switch ctx := c.(type) { case *valueCtx: if key == ctx.key { return ctx.val } c = ctx.Context case *cancelCtx: if key == &cancelCtxKey { return c } c = ctx.Context case *timerCtx: if key == &cancelCtxKey { return &ctx.cancelCtx } c = ctx.Context case *emptyCtx: return nil default: return c.Value(key) } } } func (c *cancelCtx) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err }
重点讲一下重写的
func (c *cancelCtx) Value(key any) any
方法, 如果传入的key 是cancelCtxKey
就会直接返回当前的cancelCtx, 反之就会通过包内置的value()
函数,逐层向上父节点传递,知道找到对应的key 或者找不到对于canceler具体实现的
cancel()
方法如下,Done()
同上:func (c *cancelCtx) cancel(removeFromParent bool, err error) { // 必须传入err,即导致ctx cancel 的error if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() // 如果ctx 中已经有err了,说明该ctx 已经被 cancel了 if c.err != nil { c.mu.Unlock() return // already canceled } // 关闭done 中的channel d c.err = err d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) } // 递归遍历子节点 for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil c.mu.Unlock() // 是否从父节点中删除 if removeFromParent { removeChild(c.Context, c) } }
简单的来说cancel 方法就是关闭c.done 中的channel,递归取消子节点。
其中removeChild
函数就是将子节点从父节点中的children
map 中删除, 代码如下:func removeChild(parent Context, child canceler) { p, ok := parentCancelCtx(parent) if !ok { return } p.mu.Lock() if p.children != nil { delete(p.children, child) } p.mu.Unlock() } // 向上遍历找到可取消的父节点,因为当前的父节点可能不是可取消的类型, 即不是*cancelCtx 和 *timerCtx 类型 func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() // 父节点已经取消或者不存在 if done == closedchan || done == nil { return nil, false } // 向上找到可取消的父节点并返回,*cancelCtx 或者*timerCtx 中的*cancelCtx p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { return nil, false } pdone, _ := p.done.Load().(chan struct{}) if pdone != done { return nil, false } return p, true }
-
创建cancelCtc函数:
通过如下
WithCancel
函数创建, 传入一个父节点,返回子节点和对应的取消函数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) } }
其中
propagateCancel
函数主要是向上遍历可cancel的父节点, 并挂靠在该父节点上func propagateCancel(parent Context, child canceler) { // 父节点是不可取消的类型即 emptyCtx 或者是valueCtx done := parent.Done() if done == nil { return // parent is never canceled } // 监听下父节点的Done() channel 判断是否已经取消,是的会此时直接取消当前子节点 select { case <-done: // parent is already canceled child.cancel(false, parent.Err()) return default: } // 向上找到可取消的父节点 if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() // 父节点已经取消 if p.err != nil { // parent has already been canceled child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { atomic.AddInt32(&goroutines, +1) // parentCancelCtx 函数只能找到cancelCtx 或者timerCtx 类型的父类,下面是监听其他类型的可取消的父类,当父类取消,则关联取消子类 go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) // 该case 防止父节点未取消,而当前节点已经取消导致的goroutine 泄漏 case <-child.Done(): } }() } }
3.3 timerCtx
timerCtx 其实继承类cancelCtx 并扩展了功能, 结构体定义如下:
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}
-
接口实现
timerCtx 重写了DeadLine
和cancel
方法: 具体如下
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
-
创建timerCtc函数:
// 创建超时时间段的timerCtx func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } // 创建截止时间的timerCtx func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } // 如果当前的截止时间早于父节点的截止时间, 则直接返货cancelCtx 类型的context, 因为一定是父节点早于该节点取消,或者是该节点主动调用子节点 if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() // 开起计时器,到时间自动执行取消 if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } }
3.4 valueCtx
用来传递K-V键值对的context, 底层结构体定义如下:
type valueCtx struct {
// 通过组合匿名字段,继承了Context, 在这边可以理解为上层父节点
Context
// 键值对数据
key, val any
}
valueCtx 重写了Value
方法, 即向上遍历父节点的key ,直到找到对应的key 或者找不到
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
valueCtx
的通过如下WithValue
创建:
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}
}
注意:
key 不能为nil, 同时key的类型必须是可比较类型, 函数返回的是*valueCtx
类型
4. 注意事项
官方提出了如下的几个注意事项
- 不要将context 放在结构体里,直接将context 作为函数的第一个参数传入,而且一般命名为ctx
- 不要向函数传入nil 属性的context, 如果不知道传什么就传
context.TODO()
- 不要将应该作为函数参数的类型塞到ctx中,ctx 应该存储的是一些共有的数据,例如登录的token 或者cookie等
- 同一个context 是并发安全的,可以传递到多个goroutine 中
5. 使用实例代码
// cancelCtx
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
// timerCtx
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
----------------------------------
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
// valueCtx
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))