当前位置: 首页 > news >正文

Go 语言底层(四) : 深入 Context 上下文

引言
在并发编程中,协程之间的控制, 通信与请求链路追踪,都是实际项目中不可忽视的问题。Go 语言为此提供了一个标准解决方案 —— context 包。这个包最初是为了解决 HTTP 请求的上下文管理问题,但随着 Go 的发展,它逐渐演变为并发控制与跨 API 通信的核心工具


1. 基本使用

Go 的 context 最强大的能力在于:控制协程生命周期、设置超时/截止时间,以及在调用链中传递轻量级数据。下面我们逐个介绍几种最常用的 Context 创建方法。

1.1 context.Background() 与 context.TODO()

这两个函数都用于创建根 Context,即没有父节点的起点。

ctx := context.Background()
ctx := context.TODO()

1.2 . context.WithCancel(parent)


func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 
  • parent Context : 传入的父节点Context , 如果没有 , 可以通过上诉 1.1 的方法创建。
  • ctx Context : 创建取消功能的子Context 并返回
  • cancel CancelFunc : 当调用 cancel() 时,该 Context 及其派生出的所有子 Context 都会收到取消信号

注意事项: 调用 WithCancel 后,一定要在合适的时机调用 cancel(),避免资源泄漏。

示例 :

ctx, cancel := context.WithCancel(context.Background())go func() {// 模拟一些耗时任务time.Sleep(2 * time.Second)cancel() // 主动取消 context
}()select {
case <-ctx.Done():fmt.Println("任务被取消:", ctx.Err())
}

1.3 . context.WithTimeout(parent, duration)

为 Context 设置一个超时时间,到时间后自动取消。这个方法的实现是基于WithCancel的封装的

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
  • timeout time.Duration : 设置过期时间 ,这里设置的时间段。

1.4. context.WithValue(parent, key, val)

用于在 Context 中携带请求作用域内的数据,例如用户信息、请求 ID 等。

type ctxKey stringconst userIDKey ctxKey = "userID"ctx := context.WithValue(context.Background(), userIDKey, "123456")func doSomething(ctx context.Context) {if uid, ok := ctx.Value(userIDKey).(string); ok {fmt.Println("当前用户ID:", uid)}
}

注意事项:

  • 只用于轻量级、只读的数据

  • 不建议传递业务数据、大对象、控制信号等

  • 尽量使用自定义类型作为 key,避免 key 冲突

具体原因将在原理部分解释

2. 底层实现

2.1 Context 接口

context包的核心接口如下 , 它定义了一些方法,用于获取请求范围数据、取消请求和处理超时。

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key any) any
}
  • Deadline() 方法返回截止时间和一个布尔值,指示截止时间是否已经设置。
  • Done() 方法返回一个只读的 channel,当请求被取消或超时时,该 channel 将被关闭。
  • Err() 方法返回一个错误,指示为什么请求被取消。
  • Value() 方法返回与给定key相关联的值,如果没有值,则返回 nil。

请添加图片描述

2.2 Context 树的起点

在 Go 的 context 包中,所有上下文的传播都源于某个“根 Context”,而 context.Background()context.TODO() 就是我们最常用的两个起点。

它表示一个空上下文,没有取消信号、没有截止时间、也不携带任何值。你可以从它派生出带有取消、超时或值的子 Context。

var (background = new(emptyCtx)todo       = new(emptyCtx)
)func Background() Context {return background
}func TODO() Context {return todo
}

返回的均是 emptyCtx 类型的一个实例.

type emptyCtx intfunc (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return
}func (*emptyCtx) Done() <-chan struct{} {return nil
}func (*emptyCtx) Err() error {return nil
}func (*emptyCtx) Value(key any) any {return 
}

如上 , emptyCtx 的实现是一个非常“干净”的 Context 类型 , 这使得 emptyCtx 成为所有 Context 的基础类型 , 作为所有真正可操作 Context 的根源。

请添加图片描述
基于一个父Context可以随意衍生,其实这就是一个Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

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

这个函数执行步骤如下:

  • 校验父 context 非空;
  • 创建一个cancelCtx 对象,作为子context
  • 然后调用propagateCancel 构建父子context之间的关联关系,这样当父context被取消时,子context也会被取消。
  • 将 cancelCtx 返回,连带返回一个用以终止该 cancelCtx 的闭包函数 cancel

我们来逐一介绍cancelCtx 对象, propagateCancel 方法和 闭包函数 cancel

2.3.1 cancelCtx 对象

type cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     atomic.Value          // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel call
}type canceler interface {cancel(removeFromParent bool, err error)Done() <-chan struct{}
}
  • 继承了一个 context 作为其父 context. 可见,cancelCtx 必然为某个 context 的子 context;
  • 内置了一把锁,用以协调并发场景下的资源获取;例如对这里的children 字段的操作
  • done:实际类型为 chan struct{},即用来出传递context上下文的关闭信号;
  • children:一个 set,指向 cancelCtx 的所有子 context;
  • err:记录了当前 cancelCtx 的错误. 必然为某个 context 的子 context;请添加图片描述

2.3.2 propagateCancel 方法

func propagateCancel(parent Context, child canceler) {// 如果返回nil,说明当前父`context`从来不会被取消,是一个空节点,直接返回即可。done := parent.Done()if done == nil {return // parent is never canceled}// 提前判断一个父context是否被取消,如果取消了也不需要构建关联了,把当前子节点取消掉并返回select {case <-done:// parent is already canceledchild.cancel(false, parent.Err())returndefault:}// 这里目的就是找到可以“挂”、“取消”的contextif p, ok := parentCancelCtx(parent); ok {p.mu.Lock()// 找到了可以“挂”、“取消”的context,但是已经被取消了,那么这个子节点也不需要继续挂靠了,取消即可if p.err != nil {child.cancel(false, p.err)} else {// 将当前节点挂到父节点的childrn map中,外面调用cancel时可以层层取消if p.children == nil {// 这里因为childer节点也会变成父节点,所以需要初始化map结构p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()} else {// 没有找到可“挂”,“取消”的父节点挂载,那么就开一个goroutineatomic.AddInt32(&goroutines, +1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}

propagateCancel 方法顾名思义,用以传递父子 context 之间的 cancel 事件:

  • 倘若 parent 是不会被 cancel 的类型(如 emptyCtx),则直接返回;

  • 倘若 parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err;

  • 假如 parent 是 cancelCtx 的类型,则加锁,并将子 context 添加到 parent 的 children map 当中;

  • 假如 parent 不是 cancelCtx 类型,但又存在 cancel 的能力(比如用户自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并透传 parent 的 err.
    请添加图片描述

2.3.2 cancelCtx.cancel 方法

这个方法会关闭上下文中的 Channel 并向所有的子上下文同步取消信号

func (c *cancelCtx) cancel(removeFromParent bool, err error) {// 取消时传入的error信息不能为nil, context定义了默认error:var Canceled = errors.New("context canceled")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// 用来关闭channel,通知其他协程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 = nilc.mu.Unlock()// 把当前节点从父节点中移除,只有在外部父节点调用时才会传true// 其他都是传false,内部调用都会因为c.children = nil被剔除出去if removeFromParent {removeChild(c.Context, c)}
}

该方法递归的遍历子树 , 主要保证每个节点

  • c.done.Load().(chan struct{})
  • removeFromParent

通过源码我们可以知道cancel方法可以被重复调用,是幂等的

请添加图片描述

结论 :
自此 , 我们便能够清晰的明白 , WithCancel 方法 通过创建子context 并存储到 children 字段中 , 实现了一个 context 树 . 并通过递归方法传递管道 done 字段传递关闭信号。

2.4 withDeadline、WithTimeout的实现

WithTimeout方法,它内部就是调用的WithDeadline方法 ,两者的区别 ,一个是时间点取消 , 一个是时间段取消

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) {// 不能为空`context`创建衍生contextif parent == nil {panic("cannot create context from nil parent")}// 当父context的结束时间早于要设置的时间,则不需要再去单独处理子节点的定时器了if cur, ok := parent.Deadline(); ok && cur.Before(d) {// The current deadline is already sooner than the new one.return WithCancel(parent)}// 创建一个timerCtx对象c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}// 将当前节点挂到父节点上propagateCancel(parent, c)// 获取过期时间dur := time.Until(d)// 当前时间已经过期了则直接取消if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn 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) }
}

withDeadline相较于withCancel方法也就多了一个定时器去定时调用cancel方法,这个cancel方法在timerCtx类中进行了重写,我们来看一下timerCtx类,他是基于cancelCtx的,多了两个字段:

type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}

除了继承 cancelCtx 的能力之外,新增了一个 time.Timer 用于定时终止 context;另外新增了一个 deadline 字段用于字段 timerCtx 的过期

func (c *timerCtx) cancel(removeFromParent bool, err error) {// 调用cancelCtx的cancel方法取消掉子节点contextc.cancelCtx.cancel(false, err)// 从父context移除放到了这里来做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()
}

timerCtx实现的cancel方法,内部也是调用了cancelCtx的cancel方法取消:
.请添加图片描述

2.5 WithValue的实现

withValue内部主要就是调用valueCtx类:

func WithValue(parent Context, key, val interface{}) 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}
}

valueCtx
valueCtx目的就是为Context携带键值对,因为它采用匿名接口的继承实现方式,他会继承父Context,也就相当于嵌入Context当中了

type valueCtx struct {Contextkey, val interface{}
}func (c *valueCtx) String() string {return contextName(c.Context) + ".WithValue(type " +reflectlite.TypeOf(c.key).String() +", val " + stringify(c.val) + ")"
}func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}
  • valueCtx 同样继承了一个 parent context;
  • 一个 valueCtx 中仅有一组 kv 对.
  • 实现了String方法输出Context和携带的键值对信息
  • 实现Value方法来存储键值对
    请添加图片描述
    我们在调用Context中的Value方法时会层层向上调用直到最终的根节点,中间要是找到了key就会返回,否会就会找到最终的emptyCtx返回nil。

结论 :
阅读源码可以看出,valueCtx 不适合视为存储介质,来存放大量的 kv 数据

  • 一个 valueCtx 实例只能存一个 kv 对,因此 n 个 kv 对会嵌套 n 个 valueCtx,造成空间浪费;

  • 基于 k 寻找 v 的过程是线性的,时间复杂度 O(N);

  • 不支持基于 k 的去重,相同 k 可能重复存在,并基于起点的不同,返回不同的 v. 由此得知,valueContext 的定位类似于请求头,只适合存放少量作用域较大的全局 meta 数据.


参考文档 : 小徐先生的编程世界

相关文章:

  • 鸿蒙 Stege模型 多模块应用
  • GO 基础语法和数据类型面试题及参考答案(下)
  • 解密鸿蒙系统的隐私护城河:从权限动态管控到生物数据加密的全链路防护
  • FreeRTOS任务基础知识
  • VLLM : RuntimeError: NCCL error: invalid usage
  • RT_Thread——线程管理(下)
  • 高端性能封装正在突破性能壁垒,其芯片集成技术助力人工智能革命。
  • window 显示驱动开发-如何查询视频处理功能(三)
  • 从零手写Java版本的LSM Tree (八):LSM Tree 主程序实现
  • 华为云Flexus+DeepSeek征文 | MaaS平台避坑指南:DeepSeek商用服务开通与成本控制
  • HTML5实现简洁的体育赛事网站源码
  • Nosql之Redis集群
  • 多元隐函数 偏导公式
  • 【微服务基石篇】服务间的对话:RestTemplate、WebClient与OpenFeign对比与实战
  • 我的世界Java版1.21.4的Fabric模组开发教程(十二)方块状态
  • VRRP(虚拟路由冗余协议)深度解析
  • API网关Envoy的鉴权与限流:构建安全可靠的微服务网关
  • 算法思想之广度优先搜索(BFS)及示例(亲子游戏)
  • yolo模型精度提升策略
  • OpenHarmony标准系统-HDF框架之I2C驱动开发
  • 支付宝网站支付接口/seo搜索引擎优化心得体会
  • 怀柔网站整站优化公司/网站注册流程
  • 90后做网站/百度官方网站网址是多少
  • 建设网站需要购买/如何制作网址链接
  • 朝阳区社会建设网站/win优化大师怎么样
  • 做网站一排文字怎么水平对齐/qq群推广软件