golang context底层设计探究

如果大家觉得文字版太枯燥,可以看一下我b站上分享的关于 golang context底层设计探究 ,全面详细,易懂,感谢大家观看,链接: Golang Context 底层源码拆解:3 个接口 + 4 个结构体全解析_哔哩哔哩_bilibili

Context 用于在多个函数、方法、协程、跨 API、进程之间传递信息。于 Go 1.7 版本发布,并纳入到 Go 语言标准库中。它不仅仅可以用于跨 API 场景传递上下文,同时还可以实现级联取消、超时控制等操作。

Context 是可以进行嵌套的,一个非顶级或末级的 Context 是拥有其父亲和一个或多个儿子,我们可以把它看作是一个树,也就是 Context 树

Go 官方提供了 三个接口的定义 和 四个结构体的实现

一、三个接口

1. Context接口:

规范了一个可取消,可携带数据,可传递错误的上下文对象,实现信息的 "生命周期" 和 "信息共享"

Go 复制代码
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}   // context接口 源码

1)Deadline():表示Context实现要携带一个Deadline (截止时间),当到达截止时间后该Context会被取消

2) Done(): 一个通道,用于接收该 Context 需要被取消的信号

3) Err(): 用于返回 Context 被取消的原因

4) Value(): 返回在ctx中传递保存的上下文,用于层层传递数据,例如用户的身份等等

2. Stringer接口:

定义了一个而用于生成字符串的标准方式

Go 复制代码
// 源碼
type stringer interface {
	String() string
}

String() 会将 Context 生成一个字符串,让任何ctx实现都能被打印转成人类可读的字符串, 方便调试和日志记录,如果没实现,只能看到地址

打印上述语句,如果弄i的ctx实现了String() 你会看到:

如果没实现的话看到的就是一些地址等可读性不强的字符

3. Canceler接口:

定义了了一个可取消的上下文的规范要求。TimerCtx 和 CancelCtx因为可以进行取消,所以需要实现该接口

Go 复制代码
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
	cancel(removeFromParent bool, err, cause error)
	Done() <-chan struct{}
}

1)Cancel(removeFromParent bool,err,cause error)

触发上下文的取消操作,主要功能有

  1. 关闭 Done() 通道,通知所有监听该上下文的协程

  2. 标记上下文为[已取消] 状态;

  3. 可选从父上下文的子节点列表中移除自己

  4. 记录取消的错误信息和根因

2)Done() <-chan struct{}

返回一个只读的 struct{} 通道,用于监听上下文的[取消信号] ,上下文被取消(调用Cancel()) 或超时时,该通道会被关闭,协程可通过监听该通道,实现退出

二、四个结构体

1. EmptyCtx

用于表示空上下文的类型,作为 Context链中的默认的顶级上下文,实现了下面两个接口,如图所示:Stringer和Context

Go 复制代码
// An emptyCtx is never canceled, has no values, and has no deadline.
// It is the common base of backgroundCtx and todoCtx.
type emptyCtx struct{}

func (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 nil
}

可以看出源码对ctx接口的实现都为空,本质上就是实现了一个空的结构体

2. ValueCtx:

携带了一个键值对,实现了 Context 接口的 Value 方法

实现了Stringer 和 Context两个接口:

Go 复制代码
type valueCtx struct {
	Context
	key, val any
}

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() { // 判断该 key 是否是可比较的
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

// 递归遍历 Context 链获取 key 对应的 value
func (c *valueCtx) Value(key any) any {  
    if c.key == key {  //  当前节点就名中了,直接返回值
       return c.val  
    }  
    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 withoutCancelCtx:  
          if key == &cancelCtxKey {  
             // This implements Cause(ctx) == nil  
             // when ctx is created using WithoutCancel.             return nil  
          }  
          c = ctx.c  
       case *timerCtx:  
          if key == &cancelCtxKey {  
             return &ctx.cancelCtx  
          }  
          c = ctx.Context  
       case backgroundCtx, todoCtx:  // 如果爬到最后还没有找到,就返回nil
          return nil  
       default:  
          return c.Value(key)  
       }  
    }  
}
// 从当前节点逐级向上寻找,遇到第一个匹配的key立即返回,爬到顶部还没找到就返回nil

3. CancelCtx:

cancelCtx 表示可取消的上下文。当 cancelCtx 被取消时,它也会取消任何 实现了 canceler 接口的所有子孙。

实现了Cancel,Stringer,Context三个接口:

Go 复制代码
type cancelCtx struct {
	Context // 嵌入,继承了父亲的所有方法

	mu       sync.Mutex            // 锁,保护下面字段
	done     atomic.Value          // chan struct{} 的值,懒加载创建,由第一次取消时调用关闭
	children map[canceler]struct{} // 由第一次撤销调用设置为 nil
	err      error                 // 保存上下文取消时用户设置的 error,给外部看的取消圆心,
	cause    error                 // Go 1.20+ 新增,记录取消的「根因」(可通过 `context.Cause()` 获取),给调试日志看的原因
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := withCancel(parent)
	return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, c)
	return c
}

// 实现Done方法,外部用 `select { case <-ctx.Done(): ... }` 就是读的它。
func (c *cancelCtx) Done() <-chan struct{} {
// 线程安全的读第一次
	d := c.done.Load()
	if d != nil {
	// 已存在就直接返回
		return d.(chan struct{})
	}
	// 不存在的话就枷锁创建,并发环境下可能多个协程同时第一次访问,加互斥锁保护
	c.mu.Lock()
	defer c.mu.Unlock()
	// 再读一次,仍未空才真正make(chan),然后原子写入,保证只创建一次
	d = c.done.Load()
	if d == nil {
	// 懒加载,如果没有懒加载,在每次 WithCancel 的时候就立即创建,占内存,约等于32b,如果一条context链路上又1000个中间件节点,就浪费了32kb,而且有99%的节点永远不会被监听,而懒加载就是直到有人第一次调用Done()时才会创建
		d = make(chan struct{})
		c.done.Store(d)
	}
	return d.(chan struct{})
}

1. 这里为什么要持有锁 sync.Mutex?

1. 对于 `children map[canceler]struct{}` 字段

  • 在并发情况下,多个 Goroutine 都需要对 进行读写,map 本身并发不安全,并发修改会导致 panic 。
  • `children[c] = struct{}{}`、`delete(children, c)` 这样的复合操作需要保证原子性,在并发下如果被打断,对导致 panic。

2. `err`/`cause` 赋值

`err`/`cause` 是普通 error 类型,赋值非原子操作

需保证「仅第一次取消时赋值」(幂等性),锁能防止并发赋值覆盖
3. `done` 通道的创建(懒加载)

`done` 是懒加载的(第一次调用 `Done()` 时创建),锁能防止多个协程并发创建多个 `done` 通道(保证唯一性)。
4. `Cancel` 方法的幂等性

锁能保证「仅第一次调用 `Cancel` 时执行取消逻辑」,后续调用直接返回,避免重复关闭 `done` 通道(关闭已关闭的 chan 会 panic)。

2. 为什么 `done` 要使用原子类型 `atomic.Value`?

`done` 字段不使用锁来保护(只在懒加载时使用锁),是为了提高性能,不用每次调用 `Done()` 都需要加一次锁。

4. TimerCtx:

实现了String和Context两个接口:

用于携带定时器和截止时间信息。它会嵌入一个 cancelCtx,将 Done 和 Err 方法委托给 cancelCtx。同时,它通过停止定时器后委托给 cancelCtx.cancel 方法来实现取消功能。

Go 复制代码
type timerCtx struct {
	*cancelCtx // 嵌入此字段就相当于有了Done/Err/cancel等方法
	timer *time.Timer // 定时器。到时间自动调用 cancel

	deadline time.Time // 截止时间,最晚什么时候到期
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    // context不能凭空长出来
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	// 如果你的父节点本身就比你更早到期,就不用设定时器了
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		return WithCancel(parent)
	}
	// 新建节点
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	// 把自己挂到父节点下面,同时检查父节点是不是被取消,如果是,立即把c取消掉,保持父死子随
	propagateCancel(parent, c)
	// 检查是否过期,过期直接取消
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded, nil)
		return c, func() { c.cancel(false, Canceled, nil) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
	// 启动定时器,用户可随时 calcel() 提前结束
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded, nil)
		})
	}
	// 确保用户任何时候手动执行cancel()都能提前结束
	return c, func() { c.cancel(true, Canceled, nil) }
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

其保存了一个定时器和截止时间信息。

并借助 cancelCtx 来实现取消操作。当取消 timerCtx 时,它会首先停止定时器,然后委托给 cancelCtx.cancel 方法来执行取消操作,以确保整个上下文树能够正确地被取消。然后可以看到,官方提供了两个函数来创建 TimerCtx,分别是 `WithTimeout()` 和 `WithDeadline()`

以上就是四个结构体实现对应的接口的方法,感谢大家观看!有问题欢迎在评论区讨论或者私信!

相关推荐
lkbhua莱克瓦242 小时前
基础-约束
android·开发语言·数据库·笔记·sql·mysql·约束
万邦科技Lafite2 小时前
淘宝开放API获取订单信息教程(2025年最新版)
java·开发语言·数据库·人工智能·python·开放api·电商开放平台
CoderCodingNo2 小时前
【GESP】C++五级真题(前缀和思想考点) luogu-P10719 [GESP202406 五级] 黑白格
开发语言·c++·算法
阿珊和她的猫2 小时前
页面停留时长埋点实现技术详解
开发语言·前端·javascript·ecmascript
喵了几个咪2 小时前
Go单协程事件调度器:游戏后端的无锁有序与响应时间掌控
开发语言·游戏·golang
这我可不懂2 小时前
谈谈mcp协议的实现
开发语言·qt·哈希算法
糕......2 小时前
JDK安装与Java开发环境配置全攻略
java·开发语言·网络·学习
日日行不惧千万里2 小时前
Java中Lambda Stream详解
java·开发语言·python
Trouvaille ~2 小时前
【C++篇】让错误被温柔对待(上):异常基础与核心机制
运维·开发语言·c++·后端·异常·基础入门·优雅编程