• go实战学习——context包学习理解笔记



    学习golang时,在看一下项目的源码过程中,总会发现context包被频繁的调用,为啥调用,一脸懵逼。通过对context使用进行详细的学习后整理笔记出来。

    context包

    context也就是说的上下文。
    context 包我们就用来做两件事:
    - 安全传递数据 :是指在请求执行上下文中线 程安全地传递数据,依赖于 WithValue 方法
    - 控制链路

    使用场景比较丰富: 链路追踪的 trace id 、 AB测试的标记位 、 压力测试标记位 、分库分表中间件中传递 sharding hint 、ORM 中间件传递 SQL hint 、 Web 框架传递上下文等

    打开context包源码,大致内容讲解如下:

    context包定义了context类型,它跨越API边界和进程之间携带截止日期、取消信号和其他请求范围的值。

    向服务器的传入请求应该创建一个Context,而向服务器发出的调用应该接受一个Context。它们之间的函数调用链必须传播上下文,可以选择用使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文替换它。当一个上下文被取消时,从它派生的所有上下文也被取消。

    WithCancel, WithDeadline和WithTimeout函数接受一个Context(父函数),并返回一个派生的Context(子函数)和一个CancelFunc。调用CancelFunc会取消子进程及其子进程,移除父进程对子进程的引用,并停止任何相关的计时器。未能调用CancelFunc会泄漏子进程及其子进程,直到父进程被取消或计时器触发。go vet工具检查CancelFuncs是否在所有控制流路径上使用。

    使用上下文的程序应该遵循这些规则,以保持跨包的接口一致,并启用静态分析工具来检查上下文传播:

    1. 不要将context存储在struct类型中;相反,显式地将Context传递给每个需要它的函数。Context应该是第一个参数,通常命名为ctx:
    func DoSomething(ctx context.Context, arg Arg) error {
    	// ... use ctx ...
    }
    
    • 1
    • 2
    • 3
    1. 不要传递空上下文,即使函数允许这样做。通过上下文。如果您不确定要使用哪个上下文,则使用TODO。
    2. 上下文值只用于传递进程和api的请求范围内的数据,而不是传递可选参数给函数。
    3. 相同的上下文可以传递给运行在不同goroutine中的函数;上下文对于多个goroutines同时使用是安全的。

    context 包的核心 API 有四个:

    • context.WithValue:设置键值对,并且 返回一个新的 context 实例
    • context.WithCancel
    • context.WithDeadline
    • context.WithTimeout:三者都返回一个 可取消的 context 实例,和取消函数

    注意:context 实例是不可变的,每一次都是 新创建的。

    context.WithValue 用于安全传递数据
    另外三个,WithCancelWithDeadlineWithTimeout用于控制链路

    context接口

    Context 接口核心 API 有四个:

    • Deadline :返回过期时间,如果 ok 为 false,说明没有 设置过期时间。不常用
    • Done:返回一个 channel,一般用于监听 Context 实例 的信号,比如说过期,或者正常关闭。常用
    • Err:返回一个错误用于表达 Context 发生了什么。 Canceled => 正常关闭,DeadlineExceeded => 过期超 时。比较常用
    • context.Value:取值。非常常用

    context实例

    context实例之间存在父子关系。

    • 控制从上至下:当父亲取消或者超时,所有派生的子 context 都被取消或者超时
    • 查找从下至上:当找 key 的时候,子 context 先看自己有 没有,没有则去祖先里面找
      在这里插入图片描述
      其中,父无法访问子内容,即父context无法拿到子context设置的值。

    valueCtx

    valueCtx定义如下:

    type valueCtx struct {
    	Context
    	key, val any
    }
    
    • 1
    • 2
    • 3
    • 4

    valueCtx 用于存储 key-value 数据,特点:

    • 典型的装饰器模式:在已有 Context 的基础上附加一个存 储 key-value 的功能
    • 只能存储一个 key, val:为什么不用 map?
      • map 要求 key 是 comparable 的,而我们可能用不是 comparable 的 key
      • context 包的设计理念就是将 Context

    在查找值的时候,先从自己查找,不行再找父亲的
    在这里插入图片描述

    控制

    context父亲可以控制儿子,但是儿子控制不了父亲
    context 包提供了三个控制方法, WithCancelWithDeadline WithTimeout
    三者用法大同小异:

    • WithCancel 没有过期时间,但是又需要在必要的时候取 消,使用 WithCancel
    • WithDeadline 在固定时间点过期,使用 WithDeadline
    • WithTimeout 在一段时间后过期,使用 WithTimeout
      而后便是监听 Done() 返回的 channel,不管是 主动调用 cancel() 还是超时,都能从这个 channel 里面取出来数据。后面可以用 Err() 方 法来判断究竟是哪种情况。

    context最经典的用法:控制超时,相当于我们同时监听两个 channel,一个是正常业务结束的 channel,一个Done() 返回的。

    超时控制至少两个分支:

    • 超时分支
    • 正常业务分支
      所以普遍来说 context.Context 会和 select- case 一起使用。
    // WithCancel returns a copy of parent with a new Done channel. The returned
    // context's Done channel is closed when the returned cancel function is called
    // or when the parent context's Done channel is closed, whichever happens first.
    //
    // Canceling this context releases resources associated with it, so code should
    // call cancel as soon as the operations running in this Context complete.
    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) }
    }
    
    // newCancelCtx returns an initialized cancelCtx.
    func newCancelCtx(parent Context) cancelCtx {
    	return cancelCtx{Context: parent}
    }
    
    // goroutines counts the number of goroutines ever created; for testing.
    var goroutines int32
    
    // propagateCancel arranges for child to be canceled when parent is.
    func propagateCancel(parent Context, child canceler) {
    	done := parent.Done()
    	if done == nil {
    		return // parent is never canceled
    	}
    
    	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)
    		go func() {
    			select {
    			case <-parent.Done():
    				child.cancel(false, parent.Err())
    			case <-child.Done():
    			}
    		}()
    	}
    }
    
    • 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
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    cancelCtx

    其定义如下

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    cancelCtx 也是典型的装饰器模式:在已有 Context 的基础上,加上取消的功能。
    核心实现:

    • Done 方法是通过类似于 double-check 的 机制写的。这种原子操作和锁结合的用法比较罕见。
    • 利用 children 来维护了所有的衍生节点,难点就在于它是如何维护这个衍生节点。

    children:核心是儿子把自己加进去父亲的 children 字段里面。 但是因为 Context 里面存在非常多的层级, 所以父亲不一定是 cancelCtx,因此本质上是找最近属于 cancelCtx 类型的祖先,然后儿 子把自己加进去。 cancel 就是遍历 children,挨个调用 cancel。然后儿子调用孙子的 cancel。

    核心cancel方法

    做两件事:
    • 遍历所有的 children
    • 关闭 done 这个 channel:这个符合谁创 建谁关闭的原则
    源码如下

    // cancel closes c.done, cancels each of c's children, and, if
    // removeFromParent is true, removes c from its parent's children.
    func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    	if err == nil {
    		panic("context: internal error: missing cancel error")
    	}
    	c.mu.Lock()
    	if c.err != nil {
    		c.mu.Unlock()
    		return // already canceled
    	}
    	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)
    	}
    }
    
    • 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

    timerCtx

    是装饰器模式:在已有 cancelCtx 的 基础上增加了超时的功能。
    实现要点:

    • WithTimeout 和 WithDeadline 本质一样
    • WithDeadline 里面,在创建 timerCtx 的时候利用 time.AfterFunc 来实现超时

    context包使用注意事项

    • 一般只用做方法参数,而且是作为第一个参数;
    • 所有公共方法,除非是 util,helper 之类的方法,否则都加上 context 参数;
    • 不要用作结构体字段,除非你的结构体本身也是表达一个上下文的概念。

  • 相关阅读:
    Django模版操作
    Scala面向对象
    时间轴_数据存储
    AC修炼计划(AtCoder Regular Contest 166)
    基于stm32f103系列的简单软件I2C和硬件I2C通讯
    Python自动化UI测试之Selenium基础实操
    这应该是关于回归模型最全的总结了(附原理+代码)
    树的统计问题
    每日一题 —— 882. 细分图中的可到达节点
    elasticsearch bulk数据--ES批量导入json数据
  • 原文地址:https://blog.csdn.net/weixin_44336181/article/details/126296175