推荐转到语雀阅读 www.yuque.com/anthonyzhao...
🔖Go版本:go1.25.3 windows/amd64
👉源码路径:src\context\context.go
写作背景
从学习golang开始就打算完整梳理一下上下文的设计,但是从网上看到的文章感觉写的很没有逻辑(应该是我没看到条理清晰的)。
陪产假期间,也就是本文撰写期间,leader 分享了一本Golang书籍给我,想着阅读后总要有些产出才是,于是参考书中的Context部分以及源码,写了本文,自认为逻辑算是清晰。
本文的出发点在于理清Context的设计及使用案例,重心不在实现细节。
Context 接口
context接口设计非常简单,仅关注上下文本身的功能:
Deadline()
你可以理解为上下文具有生效时间,而Deadline返回的是上下文失效的时间。Done()
是用来阻塞等待上文失效的,当上下文失效后,可立刻感知到。无需采用轮询的方式。Err()
获取上下文失效的原因。Value()
获取上下文携带的kv值。
接口不麻烦,宏观上看,其实上下文就提供了两个功能
- 支持失效,包括随时失效和定时失效;失效原因相关功能。
- 在传递路径上携带kv值
每个功能都是派生实现的。
最基础的Ctx
源码中对于context接口的直接实现只有 emtpyCtx, 我们常用的 backgroundCtx 和 todoCtx 则是组合了 emptyCtx。由于Golang没有集成,这对于Golang来说也算是间接实现了 context 接口。
从上图来看,backgroundCtx 和 todoCtx 似乎没有任何区别,完全与 emptyCtx 等价,但其使用场景是不同的
todoCtx
: 常用于用作待办,即当下是想传递上下文的,但是不明确传递哪个上下文更好,也可以理解为是占位用的。backgroundCtx
:通常用作根上下文使用(上下文是一个树形结构,后边会详细解释)
这里贴一下 emptyCtx 的实现
go
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
}
从中可以发现 emptyCtx 或者说 background 虽然实现了 context 接口,但是接口中定义的功能完全没有实现:不具有失效时间、不会失效、不能传递值。
事实上 Golang 上下文的完整功能是通过不同的 结构体来实现的,我们接下来慢慢了解。
valueCtx

从类图角度valueCtx组合了context接口。Golang为valueCtx单独提供了一个构造函数
func WithValue(parent Context, key, val any) Context
调用该构造函数后会返回一个新的context实例,新的实例包含父context实例。
从类图中可以看到,valueCtx 相当于是重写了 Value() 方法,根据Go语言特性,调用 Value() 方法时,真正使用的是 valueCtx 的实现;而调用其他三个方法时则会使用 组合context 的实现(即,父context实例的实现)
go
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "key", "anthonyzhao")
fmt.Println(ctx.Value("key"))
}
树形结构的context
构造 ValueCtx 时,Golang提供了一个构造器,生成一个新的实例。事实上,对于上下文的每一个功能,Golang都提供了构造器。构造器中都会传入父context实例。还是以 ValueCtx 为例,如果多次调用,就会形成一颗context树,但是是指向父节点的树。树种的节点可以是各种直接、间接实现ctx的实例,包括但不限于 backgroundCtx、ValueCtx。

思考一下:Context 是如何获取上游实例设置的KV值的
当然是通过对树的遍历啦,每个节点都持有对父节点的指针,查找kv值时向上遍历即可。
cancelCtx
cancelCtx 结构
这里首先给出 cancelCtx 的代码结构,与以往类似都组合了 context 接口,可以理解为树形结构中的父节点。
还可以看到,cancelCtx 中还包含了 children,从字面理解应该是一个 context 树中当前节点的子节点集合。但是,奇怪的是为什么不是context的集合,而是 canceler 的集合。这就要从 cancelCtx 的设计本意来理解了。
go
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
mu sync.Mutex // 保护如下的字段,考虑并发的场景
done atomic.Value // chan struct{}, 懒创建,首次调用cancel()时关闭
children map[canceler]struct{} // 调用 cancel() 后设置为 nil
err atomic.Value // 调用 cancel() 后设置具体值
cause error // 调用 cancel() 后设置具体值
}
注意:注释中说当前上下文实例被取消时,其子节点实例均会被取消。
通过接口的设计可以分析出上下文是支持失效的,cancelCtx的设计目的是让上下文具有**随时可以失效**的能力。这种能力被封装为了一个接口,就是 canceler。
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{}
}

那为什么要单独搞一个接口呢?而不是让context接口也具有随时取消的能力?
除了设计上更加灵活以外,我理解还应该是从安全的角度考虑,存在某些场景不应该支持上下文随时取消。
取消后下游如何感知
考虑一个问题,当上游取消时,下游是如何立刻感知的呢?这就是 Done() <-chan struct{}
提供能的能力。
下游可以监听 Done() 返回的channel,当 上下文取消时 该 channel 会被关闭。channel未关闭则会阻塞协程,关闭后则能够读取值,不会阻塞协程,这是goalng channel 的特性。
本文的重点不在于取消的细节,你大致可以理解为Done的实现如下,真正的实现包含设计思想在里边,你可以自己研究下。
go
type cancelCtx struct {
Context
mu sync.Mutex // 保护如下的字段,考虑并发的场景
done atomic.Value // chan struct{}, 懒创建,首次调用cancel()时关闭
children map[canceler]struct{} // 调用 cancel() 后设置为 nil
err atomic.Value // 调用 cancel() 后设置具体值
cause error // 调用 cancel() 后设置具体值
}
func (c *cancelCtx) Done() <-chan struct{} {
d = c.done.Load() // 返回的是结构体中的done字段
return d.(chan struct{})
}
如下是一个示例,实现的是超时自动取消功能。你可以多尝试几次,会有不同的输出。
go
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
func HandelRequest(ctx context.Context) {
finish := make(chan struct{})
go WriteDatabase(ctx, finish)
for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest canceled.")
return
case <-finish:
fmt.Println("WriteDatabase Done.")
return
default:
fmt.Println("HandelRequest running")
time.Sleep(500 * time.Millisecond)
}
}
}
func WriteDatabase(ctx context.Context, finish chan struct{}) {
// 随机休眠2s-6s左右
time.Sleep(time.Second * time.Duration(rand.Intn(4)+4))
finish <- struct{}{}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go HandelRequest(ctx)
time.Sleep(5 * time.Second)
cancel()
//Just for test whether sub goroutines exit or not
time.Sleep(5 * time.Second)
}
cancelCtx 对象使用 func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
,与之类似的还有一个 WithCancelCause,在执行cancel()时可以传入取消的原因。
go
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
timerCtx
上边说的cancelCtx是支持随时取消,而timerCtx则是定时取消,就是到达Deadline时取消。
其对应的构造函数主要有两个,含义也非常简单
go
// 指定失效的时间点
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
// 指定多久后失效
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
timerCtx 的底层实现其实复用的 cancelCtx, 这完全说的通。只不过是 cancelCtx 的 cancel() 方法是通过代码编排在某个case下调用,而timerCtx 是通过 golang timer 的能力来定时调用cancel()。
现在,上边请求超时的例子main函数就可以改为如下的样子
go
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
func main() {
ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
go HandelRequest(ctx)
//Just for test whether sub goroutines exit or not
time.Sleep(10 * time.Second)
}
func HandelRequest(ctx context.Context) {
finish := make(chan struct{})
go WriteDatabase(ctx, finish)
for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest canceled.")
return
case <-finish:
fmt.Println("WriteDatabase Done.")
return
default:
fmt.Println("HandelRequest running")
time.Sleep(500 * time.Millisecond)
}
}
}
func WriteDatabase(ctx context.Context, finish chan struct{}) {
// 随机休眠4s-6s左右
time.Sleep(time.Second * time.Duration(rand.Intn(4)+2))
finish <- struct{}{}
}