golang -context

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{}
    }
    
    1. Deadline方法是获取设置的截止时间的意思,第一个返回值是截止时间,到了这个时间点,Context会自动发起取消请求; 第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
    2. Done方法返回一个只读的chan,类型为struct{},在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求, 我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。之后,Err 方法会返回一个错误,告知为什么 Context 被取消
    3. Err方法返回取消的错误原因,因为什么Context被取消,例如 被取消或者超时
    4. 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

实现了Contextcanceler 接口, 结构体定义如下

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 重写了DeadLinecancel 方法: 具体如下
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. 注意事项

官方提出了如下的几个注意事项

  1. 不要将context 放在结构体里,直接将context 作为函数的第一个参数传入,而且一般命名为ctx
  2. 不要向函数传入nil 属性的context, 如果不知道传什么就传context.TODO()
  3. 不要将应该作为函数参数的类型塞到ctx中,ctx 应该存储的是一些共有的数据,例如登录的token 或者cookie等
  4. 同一个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"))

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • 为什么需要context 在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作。...
    陈二狗想吃肉阅读 534评论 0 2
  • [TOC] Golang Context分析 Context背景 和 适用场景 golang在1.6.2的时候还没...
    AllenWu阅读 11,525评论 0 30
  • 参考Go语言实战笔记(二十)| Go ContextGolang context初探 一、WaitGroup 这是...
    合肥黑阅读 585评论 0 10
  • overview Package context defines the Context type, which ...
    wncbbnk阅读 564评论 0 0
  • context包专门用来简化处理单个请求的多个goroutine之间与请求域的数据、取消信号、截止时间等相关操作。...
    wz998阅读 3,739评论 0 3