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. 不要传递空上下文,即使函数允许这样做。通过上下文。如果您不确定要使用哪个上下文,则使用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
}

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():
			}
		}()
	}
}

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
}

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)
	}
}

timerCtx

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

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

context包使用注意事项

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值