vault backup: 2026-06-07 12:14:39
This commit is contained in:
+203
-396
@@ -1,468 +1,275 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go原理深入
|
||||
- channel原理
|
||||
tags: [go, golang, go-principle, context]
|
||||
create time: 2026-06-07 15:30
|
||||
---
|
||||
|
||||
# context原理
|
||||
# Context 底层原理
|
||||
|
||||
## context是什么
|
||||
## 概述
|
||||
|
||||
context是go语言在1.7引入的一个用于goroutine之间传递信息的并发安全的包,context可以翻译为上下文,其在项目中主要是用于上下与下层goroutine的取消控制以及数据共享,也是go语言中goroutine之间通信的一种方式,其底层是借助channl与sync.Mutex实现的。
|
||||
本文从源码角度解析 Go `context` 包的四种实现(emptyCtx / cancelCtx / timerCtx / valueCtx),以及取消传播链和值传递链的底层机制。Context 是 Go 并发编程中控制取消和数据共享的核心工具,理解其实现能让你写出更健壮的并发程序。
|
||||
|
||||
关于context的用法我们在前一章节并发实践里已经做过介绍,本章主要介绍一下context的底层原理
|
||||
> [!question] ❓ 思考
|
||||
> 为什么 `WithValue` 派生的 context 层层嵌套形成链表,而不是用 map 存储?当父 context 被取消时,子 context 是如何级联收到信号的?
|
||||
|
||||
## context的底层实现
|
||||
## 正文
|
||||
|
||||
与context相关的源码基本都在src/context/context.go中,我们通过源码来看一下,context的底层究竟做了些什么
|
||||
### 一、Context 的类型体系
|
||||
|
||||
context在底层实现上其实用到了2个接口,对这个接口的4种实现,以及提供了6个方法
|
||||
```mermaid
|
||||
graph TB
|
||||
C["Context interface<br/>Deadline/Done/Err/Value"] --> empty["emptyCtx<br/>根 context"]
|
||||
C --> cancel["cancelCtx<br/>可取消"]
|
||||
C --> timer["timerCtx<br/>可取消 + 定时"]
|
||||
C --> value["valueCtx<br/>键值对传递"]
|
||||
|
||||
**接口:**
|
||||
timer -.嵌入.-> cancel
|
||||
cancel -.嵌入.-> C
|
||||
value -.嵌入.-> C
|
||||
|
||||
| 接口名 | 说明 |
|
||||
| -------- | ------------------------------------ |
|
||||
| Context | context的接口定义,规定context的实现必须包含的四个基本方法 |
|
||||
| canceler | context的取消接口,其中定义了两个方法 |
|
||||
B["Background()"] --> empty
|
||||
T["TODO()"] --> empty
|
||||
WC["WithCancel(parent)"] --> cancel
|
||||
WD["WithDeadline/Timeout(parent)"] --> timer
|
||||
WV["WithValue(parent, key, val)"] --> value
|
||||
|
||||
**实现:**
|
||||
|
||||
context接口的四种实现
|
||||
|
||||
| 结构名 | 说明 |
|
||||
| ---------- | ------------------------------------ |
|
||||
| emptyCtx | 一个空的context,用作根context |
|
||||
| cancelCtx | 可以通过取消函数来取消context |
|
||||
| timerCtx | 可以通过定时器和deadline来定时取消contextvalueCtx |
|
||||
| valueCtx | 类似于map,可以用来存储key/valuez键值对 |
|
||||
|
||||
**方法:**
|
||||
|
||||
| 函数名 | 说明 |
|
||||
| ------------- | ----------------------- |
|
||||
| Background | 返回一个根context即emptyCtx |
|
||||
| TODO | 也是返回一个根context即emptyCtx |
|
||||
| WithCancel | 派生出一个cancelCtx |
|
||||
| WithDeadline | 派生出一个timerCtx |
|
||||
| WithTimeout | 派生出一个timerCtx |
|
||||
| WithValue | 派生出一个valueCtx |
|
||||
|
||||
下面我们将逐一解读这几个结构及其实现方法
|
||||
|
||||
## 接口说明
|
||||
|
||||
### context接口
|
||||
|
||||
首先还是回顾一下context接口,context的接口定义如下:
|
||||
|
||||
```go
|
||||
type Context interface {
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
Done() <-chan struct{}
|
||||
Err() error
|
||||
Value(key interface{}) interface{}
|
||||
}
|
||||
style C fill:#e3f2fd
|
||||
style empty fill:#e8f5e9
|
||||
style cancel fill:#fff9c4
|
||||
style timer fill:#fff3e0
|
||||
style value fill:#fce4ec
|
||||
```
|
||||
|
||||
接口提供了四个方法
|
||||
四个结构体的核心职责:
|
||||
|
||||
* `Deadline`: 返回 context.Context 被取消的时间,即截止时间;
|
||||
| 类型 | 可取消 | 有 Deadline | 存值 | 用途 |
|
||||
|------|--------|-------------|------|------|
|
||||
| `emptyCtx` | 否 | 否 | 否 | 根 context (Background/TODO) |
|
||||
| `cancelCtx` | 是 | 否 | 否 | WithCancel 派生 |
|
||||
| `timerCtx` | 是 | 是 | 否 | WithDeadline/Timeout 派生 |
|
||||
| `valueCtx` | 否 | 否 | 是 | WithValue 派生 |
|
||||
|
||||
* `Done`: 返回一个 Channel,当Context被取消或者到达截止时间,这个 Channel 就会被关闭,表示context结束,多次调用 Done 方法返回的channel是同一个
|
||||
### 二、取消传播链:cancelCtx
|
||||
|
||||
* `Err`: 返回 context.Context 结束的原因
|
||||
|
||||
* 4.`Value` :从 context.Context 中获取键对应的值,类似于map的get方法,对于同一个context,多次调用 Value 并传入相同的 Key 会返回相同的结果,如果没有对应的`key`,则返回`nil`,键值对是通过WithValue方法写入
|
||||
|
||||
### canceler接口
|
||||
|
||||
**canceler接口的源码定义如下**
|
||||
|
||||
```go
|
||||
type canceler interface {
|
||||
cancel(removeFromParent bool, err error) // 创建cancel接口实例的goroutine 调用cancel方法通知被创建的goroutine退出
|
||||
Done() <-chan struct{} // 返回一个channel,后续被创建的goroutine通过监听这个channel的信号来完成退出
|
||||
}
|
||||
```
|
||||
|
||||
canceler接口主要用于取消方法的实现,如果一个示例既实现了context接口又实现了canceler接口,那么这个context就是可以本取消的,比如cancelCtx 和timerCtx。如果仅仅只是实现了context接口,而没有实现canceler,就是不可取消的,比如emptyCtx 和valueCtx。
|
||||
|
||||
## contex实现
|
||||
|
||||
在context报下对context接口有四种基本的实现,即emptyCtx ,cancelCtx ,timerCtx,valueCtx
|
||||
|
||||
### emptyCtx
|
||||
|
||||
首先看一下emptyCtx 这个最基本的实现,emptyCtx 虽然实现了context接口,但是不具备任何功能,因为实现很简单,基本都是直接返回空值。虽然emptyCtx 没有任何功能,但他还是有作用的,一般用它作为根context来派生出有实际用处的context。要想创建有实际功能的context,要使用后续提供的一系列with方法来派生出新的context,这个在前面讲context用法的时候已经做过介绍,就不再过多赘述。
|
||||
|
||||
emptyCtx 的相关源码:
|
||||
|
||||
```go
|
||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
||||
// struct{}, since vars of this type must have distinct addresses.
|
||||
type emptyCtx int
|
||||
|
||||
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 的实现没有做任何操作,就是一个整形结构。这个空的emptyCtx 会在两个创建根context得函数被用到
|
||||
|
||||
```go
|
||||
func Background() Context {
|
||||
return background
|
||||
}
|
||||
|
||||
func TODO() Context {
|
||||
return todo
|
||||
}
|
||||
```
|
||||
|
||||
而这里background和todo其实就是返回一个emptyCtx&##x20;
|
||||
|
||||
```go
|
||||
var (
|
||||
background = new(emptyCtx)
|
||||
todo = new(emptyCtx)
|
||||
)
|
||||
```
|
||||
|
||||
在写代码的时候,我么你调用这两个函数其实Background()函数或者TODO()函数创建最顶层的context其实就是获取一个emptyCtx。
|
||||
|
||||
###cancelCtx
|
||||
|
||||
cancelCtx结构定义如下:
|
||||
#### 数据结构
|
||||
|
||||
```go
|
||||
type cancelCtx struct {
|
||||
Context // 组合了一个Context ,所以cancelCtx 一定是context接口的一个实现
|
||||
mu sync.Mutex // 互斥锁,用于保护以下三个字段
|
||||
// value是一个chan struct{}类型,原子操作做锁优化
|
||||
done atomic.Value
|
||||
// key是一个取消接口的实现,map其实存储的是当前canceler接口的子节点,当前context被取消时,会遍历子节点发送取消信号
|
||||
children map[canceler]struct{}
|
||||
err error // context被取消的原因
|
||||
Context // 嵌入父 context
|
||||
mu sync.Mutex // 保护以下字段
|
||||
done atomic.Value // chan struct{},nil 或未关闭 → 未取消;已关闭 → 已取消
|
||||
children map[canceler]struct{} // 子 canceler 集合
|
||||
err error // 取消原因
|
||||
}
|
||||
```
|
||||
|
||||
下面看一下其各个方法的具体实现,首先看一下`Done()`方法:
|
||||
#### Done 通道的懒汉创建
|
||||
|
||||
```go
|
||||
func (c *cancelCtx) Done() <-chan struct{} {
|
||||
d := c.done.Load()
|
||||
if d != nil {
|
||||
return d.(chan struct{})
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
d = c.done.Load()
|
||||
if d == nil {
|
||||
d = make(chan struct{})
|
||||
c.done.Store(d)
|
||||
}
|
||||
return d.(chan struct{})
|
||||
d := c.done.Load()
|
||||
if d != nil { return d.(chan struct{}) }
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
d = c.done.Load() // 双重检查
|
||||
if d == nil {
|
||||
d = make(chan struct{})
|
||||
c.done.Store(d)
|
||||
}
|
||||
return d.(chan struct{})
|
||||
}
|
||||
```
|
||||
|
||||
代码很简单,其实就是采用”懒汉模式“创建一个struct{}类型的管道返回,从类型可以看出这个channel是只读的,不能往里面写数据,所以应该避免直接读取这个channel,会发生阻塞。所以在使用上要配合select来非阻塞读取,由于是只读的,所以只有在一种情况下会读到值,那就是关闭这个channel的时候会读到零值。利用这个而特性就可以实现关闭的消息通知。
|
||||
注意这个 channel 是**只读的**——只有父 context 关闭它,子 goroutine 通过 `select` 监听它来感知取消信号。
|
||||
|
||||
再看一下其 cancel() 方法的实现:
|
||||
#### 取消逻辑:递归级联
|
||||
|
||||
```go
|
||||
// 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 { // context被取消的原因,必传,否则panic
|
||||
panic("context: internal error: missing cancel error")
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.err != nil { // 在赋值这个err之前,c.err已经有值了,说明已经被调用过cancel函数了,c这个context已经被取消
|
||||
c.mu.Unlock()
|
||||
return // already canceled
|
||||
}
|
||||
c.err = err // 赋值err信息
|
||||
d, _ := c.done.Load().(chan struct{}) // 获取通知管道
|
||||
if d == nil {
|
||||
c.done.Store(closedchan)
|
||||
} else {
|
||||
close(d) // 关闭管道
|
||||
}
|
||||
// 遍历当前context的所有子节点,调用取消函数
|
||||
for child := range c.children {
|
||||
// NOTE: acquiring the child's lock while holding parent's lock.
|
||||
child.cancel(false, err) // 递归取消子context
|
||||
}
|
||||
c.children = nil // 取消动作完成之后,孩子节点置空
|
||||
c.mu.Unlock()
|
||||
|
||||
if removeFromParent {
|
||||
removeChild(c.Context, c) // 将自身从父节点children map种移除
|
||||
c.mu.Lock()
|
||||
if c.err != nil { c.mu.Unlock(); return } // 已取消
|
||||
c.err = err
|
||||
d, _ := c.done.Load().(chan struct{})
|
||||
if d == nil {
|
||||
c.done.Store(closedchan) // 预创建的 closed chan
|
||||
} else {
|
||||
close(d) // 关闭通道,通知所有监听者
|
||||
}
|
||||
for child := range c.children {
|
||||
child.cancel(false, err) // 递归取消子节点
|
||||
}
|
||||
c.children = nil
|
||||
c.mu.Unlock()
|
||||
if removeFromParent {
|
||||
removeChild(c.Context, c) // 从父节点移除自己
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
cancel不仅取消当前context,还会遍历当前context的所有子context,递归取消,递归取消玩当前context的所有子context后,会将自身从父节点children map种移除,移除函数removeChild源码如下:
|
||||
取消流程:
|
||||
|
||||
```go
|
||||
// removeChild removes a context from its parent.
|
||||
func removeChild(parent Context, child canceler) {
|
||||
p, ok := parentCancelCtx(parent)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
if p.children != nil {
|
||||
delete(p.children, child) // 从父context的children中移除
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
```mermaid
|
||||
flowchart TD
|
||||
P["父 cancelCtx"] --> C1["子 cancelCtx 1"]
|
||||
P --> C2["子 cancelCtx 2"]
|
||||
C1 --> C1a["孙 cancelCtx"]
|
||||
C2 --> C2a["孙 valueCtx"]
|
||||
|
||||
style P fill:#ffebee
|
||||
style C1 fill:#fff3e0
|
||||
style C2 fill:#fff3e0
|
||||
style C1a fill:#e8f5e9
|
||||
style C2a fill:#e8f5e9
|
||||
|
||||
click P "触发 cancel()"
|
||||
click C1 "级联取消"
|
||||
click C2 "级联取消"
|
||||
```
|
||||
|
||||
移除前后效果如下图所示:
|
||||
调用 `cancel()` 后:
|
||||
1. 关闭自己的 `done` channel → 所有监听该 channel 的 goroutine 收到信号
|
||||
2. 递归取消所有子节点
|
||||
3. 将自己从父节点的 children 中移除
|
||||
|
||||

|
||||
|
||||
在用户层面,创建cancelCtx的方法其实我们你之前也接触过,就是withCancel方法,在平常代码中,我们一般用这个方法来派生一个可以用cancel取消函数取消的context,常规用法如下:
|
||||
### 三、父子关联:propagateCancel
|
||||
|
||||
```go
|
||||
ctx,cancel := context.WithCancel(context.Background())
|
||||
```
|
||||
|
||||
下面继续跟一下这个WithCancel函数的源码:
|
||||
|
||||
```go
|
||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||
if parent == nil { // 传入的父context不能为空,否则报panic
|
||||
panic("cannot create context from nil parent")
|
||||
}
|
||||
c := newCancelCtx(parent) // 这里就会创建一个cancelCtx
|
||||
propagateCancel(parent, &c) // 这里主要是关联父context ctx和子congtxt c的逻辑
|
||||
return &c, func() { c.cancel(true, Canceled) } // 具体的取消函数cancel的实现
|
||||
}
|
||||
```
|
||||
|
||||
前面说了调用cancelFunc函数可以级联取消子context,那么为什么可以级联取消呢?propagateCancel函数就是用来做这个工作的,他将父context和子context关联起来,具体的关联逻辑,我们通过源码来分析:
|
||||
|
||||
```go
|
||||
// propagateCancel arranges for child to be canceled when parent is.
|
||||
func propagateCancel(parent Context, child canceler) {
|
||||
done := parent.Done() // 获取父context的通信管道 chan struct{}
|
||||
if done == nil { // done为空,说明父context不会被取消
|
||||
return // parent is never canceled
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done: // 通信管道收到了消息,说明父context已经被取消,不用重复取消了
|
||||
// parent is already canceled
|
||||
child.cancel(false, parent.Err()) // 但是父context已经取消,这里子context也应该要取消,由于还没有关联上,所以主动调用cancel取消关联
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if p, ok := parentCancelCtx(parent); ok { // 从父context中提取出cancelCtx结构
|
||||
p.mu.Lock()
|
||||
if p.err != nil { // 加锁后双重检查,再次检查父context有没有被取消
|
||||
// parent has already been canceled
|
||||
child.cancel(false, p.err) // 父context被取消,主动取消子context
|
||||
} else { // 父context没有被取消
|
||||
if p.children == nil {
|
||||
p.children = make(map[canceler]struct{}) // 创建父context的children map
|
||||
}
|
||||
p.children[child] = struct{}{} // 把当前子context加入到children map里面
|
||||
}
|
||||
p.mu.Unlock()
|
||||
} else { // 从父context中没有提取出cancelCtx结构
|
||||
atomic.AddInt32(&goroutines, +1)
|
||||
go func() { // 新起一个goroutine监控父子context的通信管道有没有取消信号
|
||||
select {
|
||||
case <-parent.Done():
|
||||
child.cancel(false, parent.Err())
|
||||
case <-child.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
done := parent.Done()
|
||||
if done == nil {
|
||||
return // 父节点永远不会被取消
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
child.cancel(false, parent.Err()) // 父已取消,子直接取消
|
||||
return
|
||||
default:
|
||||
}
|
||||
// 尝试从父提取 cancelCtx
|
||||
if p, ok := parentCancelCtx(parent); ok {
|
||||
p.mu.Lock()
|
||||
if p.err != nil {
|
||||
child.cancel(false, p.err)
|
||||
} else {
|
||||
if p.children == nil {
|
||||
p.children = make(map[canceler]struct{})
|
||||
}
|
||||
p.children[child] = struct{}{}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
} else {
|
||||
// 父不是标准 cancelCtx,启动 goroutine 监控
|
||||
atomic.AddInt32(&goroutines, +1)
|
||||
go func() {
|
||||
select {
|
||||
case <-parent.Done():
|
||||
child.cancel(false, parent.Err())
|
||||
case <-child.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
看一下这个提取父context的cancelCtx结构的parentCancelCtx方法:
|
||||
三种情形:
|
||||
|
||||
```go
|
||||
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
||||
done := parent.Done()
|
||||
// 从父context的取消信息管道为空,说明父context不会被取消
|
||||
// closedchan is a reusable closed channel.
|
||||
// var closedchan = make(chan struct{})
|
||||
// done == closedchan,表明ctx不是标准的 cancelCtx,可能是自定义的结构实现了 context.Context 接口
|
||||
if done == closedchan || done == nil { //
|
||||
return nil, false
|
||||
}
|
||||
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) // 通过context的value方法从父context中提取出cancelCtx
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
pdone, _ := p.done.Load().(chan struct{}) // 判断父context里的通信管道和cancelCtx里的管道是否一致
|
||||
if pdone != done { // 不一致,表明parent不是标准的cancelCtx
|
||||
return nil, false
|
||||
}
|
||||
return p, true // 返回cancelCtx
|
||||
}
|
||||
```
|
||||
| 父 context 情况 | 处理方式 |
|
||||
|----------------|---------|
|
||||
| `Done() == nil`(永远不取消) | 无需关联 |
|
||||
| 能提取出 `cancelCtx` | 直接加入 children map |
|
||||
| 不能提取 `cancelCtx`(如 valueCtx 链中的某层) | 起一个 goroutine 监控 |
|
||||
|
||||
总结一下通过WithCancel函数在派生可取消的子context的过程中,通过propagateCancel函数关联父子context可能遇到的几种情形:
|
||||
### 四、值传递链:valueCtx
|
||||
|
||||
1. 父context的通信管道done为空或者已经被取消,就不用关联了,直接取消当前子context即可‘’
|
||||
|
||||
2. 父context可以被取消,但是还未被取消,并且父context可以提取出标准的cancelCtx结构,则创建父context的children map,将当前子context加入到这个map中
|
||||
|
||||
3. 父context可以被取消,但是还未被取消,父context不能提取出标准的cancelCtx结构,新起一个goroutine监控父子context的通信管道有没有取消信号
|
||||
|
||||
### timerCtx
|
||||
|
||||
timerCtx在cancelCtx 的基础上,又提供了截止时间的功能,不仅拥有像cancelCtx 一样,可以通过调用取消函数cancelFun来取消子context的方式,还可以设置一个截止时间deadline ,在 deadline 到来时,自动取消 context。
|
||||
|
||||
首先看一下timerCtx的结构定义:
|
||||
|
||||
```go
|
||||
type timerCtx struct {
|
||||
cancelCtx
|
||||
timer *time.Timer // Under cancelCtx.mu.
|
||||
deadline time.Time
|
||||
}
|
||||
```
|
||||
|
||||
看到它内置了cancelCtx,所以cancelCtx拥有的功方法,他可以调用cancelCtx的方法,能够主动取消context,再看一下timerCtx自身的cancel方法实现:
|
||||
|
||||
```go
|
||||
func (c *timerCtx) cancel(removeFromParent bool, err error) {
|
||||
c.cancelCtx.cancel(false, err) // 直接调用cancelCtx的cancel方
|
||||
if removeFromParent {
|
||||
// Remove this timerCtx from its parent cancelCtx's children.
|
||||
removeChild(c.cancelCtx.Context, c) // 将当前子context从父context中删除
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.timer != nil { // 要关闭掉定时器,因为手动取消过一次了,如果不关闭,在deadline 到来时,不会再次取消,造成错误
|
||||
c.timer.Stop()
|
||||
c.timer = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
```
|
||||
|
||||
同样在用户层面,我们一般通过WithTimeout或者WithDeadline来创建一个timerCtx
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithDeadline(context.Background(),time.Now().Add(4*time.Second)) // 截止时间当前时间4s后
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) // 超时时间为4s后
|
||||
```
|
||||
|
||||
在WithTimeout内部其实也是调用了WithDeadline,所以只用分析WithDeadline方法即可:
|
||||
|
||||
```go
|
||||
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
|
||||
if parent == nil { // 父context为空,直接报panic
|
||||
panic("cannot create context from nil parent")
|
||||
}
|
||||
// 如果父context的deadline早于这里要设置的子context的截止时间
|
||||
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
|
||||
// 直接取消父context即可,不需要再管子context的取消时间,直接构建一个可以取消的子context
|
||||
// 因为父context的到期时间早于子context,当父context被取消的时候,这个子context肯定会被级联取消
|
||||
return WithCancel(parent)
|
||||
}
|
||||
// 创建timerCtx对象
|
||||
c := &timerCtx{
|
||||
cancelCtx: newCancelCtx(parent),
|
||||
deadline: d,
|
||||
}
|
||||
propagateCancel(parent, c) // 关联父子context
|
||||
dur := time.Until(d) // 获取距离设置的子context过期时间的时间差
|
||||
if dur <= 0 { // 时间差小于0,表示已经过期了,直接取消
|
||||
c.cancel(true, DeadlineExceeded) // deadline has already passed
|
||||
return c, func() { c.cancel(false, Canceled) }
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.err == nil {
|
||||
// 根据时间差,创建一个定时器,到deadline的时候定时触发取消
|
||||
c.timer = time.AfterFunc(dur, func() {
|
||||
c.cancel(true, DeadlineExceeded)
|
||||
})
|
||||
}
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
```
|
||||
|
||||
所以,父context未取消的情况下,在创建timerCtx的时候有两种情况:
|
||||
|
||||
设置的截止时间晚于父context的截止时间,则不会创建timerCtx,会直接创建一个可取消的context,因为父context的截止时间更早,会先被取消,父context被取消的时候会级联取消这个子context
|
||||
|
||||
设置的截止时间早于父context的截止时间,会创建一个正常的timerCtx
|
||||
|
||||
### valueCtx
|
||||
|
||||
valueCtx的作用与上述三个context有点不同,他不是用于父子context之间的取消的,而是用于数据共享。作用类似于一个map,不过数据的存储和读取是在两个context,用于goroutine之间的数据传递。
|
||||
|
||||
valueCtx的结构定义如下:
|
||||
#### 数据结构
|
||||
|
||||
```go
|
||||
type valueCtx struct {
|
||||
Context
|
||||
key, val interface{}
|
||||
Context // 父 context
|
||||
key, val interface{} // 当前层的键值对
|
||||
}
|
||||
```
|
||||
|
||||
valueCtx内置了Context,所以他也是一个context接口的实现,但是其没有实现canceler接口,所以他不能用作context的取消,valueCtx实现了`String()`方法和`Value`方法,`String()`比较简单,就不细看了,下面看一下
|
||||
每个 `valueCtx` 只存**一对**键值对,多层 `WithValue` 会形成嵌套链表:
|
||||
|
||||
`Value`方法
|
||||
```mermaid
|
||||
flowchart LR
|
||||
V2["valueCtx2<br/>key2=val2"] --> V1["valueCtx1<br/>key1=val1"]
|
||||
V1 --> E["emptyCtx<br/>Background"]
|
||||
style V2 fill:#fce4ec
|
||||
style V1 fill:#fff3e0
|
||||
style E fill:#e8f5e9
|
||||
```
|
||||
|
||||
查找过程:
|
||||
|
||||
```go
|
||||
func (c *valueCtx) Value(key interface{}) interface{} {
|
||||
if c.key == key {
|
||||
return c.val
|
||||
}
|
||||
return c.Context.Value(key)
|
||||
if c.key == key {
|
||||
return c.val
|
||||
}
|
||||
return c.Context.Value(key) // 向上递归查找
|
||||
}
|
||||
```
|
||||
|
||||
方法很简单,就是向上递归的查找key所对应的value,如果找到则直接返回 value,否则查找该context的父context,一直顺着 context 向上,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。查找过程如下图:
|
||||
|
||||

|
||||
|
||||
从定义可以出valueCtx中存储着一对键值对,具体是怎么用的呢?同样我们一般使用withValue方法派生出一个valueCtx
|
||||
> [!warning] ⚠️ 性能提示
|
||||
> Value 查找是 O(n) 线性搜索(n = 嵌套层数)。过深的嵌套会影响性能,建议控制在合理范围内。同时,key 应避免使用容易碰撞的类型,推荐使用自定义不可比较类型。
|
||||
|
||||
```go
|
||||
ctx := context.WithValue(context.Background(),"key1","value1")
|
||||
// 推荐的 key 定义方式
|
||||
type ctxKey string
|
||||
const myKey ctxKey = "my-key"
|
||||
ctx := context.WithValue(ctx, myKey, value)
|
||||
```
|
||||
|
||||
withValue函数源码如下:
|
||||
### 五、TimerCtx:定时取消
|
||||
|
||||
```go
|
||||
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}
|
||||
type timerCtx struct {
|
||||
cancelCtx
|
||||
timer *time.Timer
|
||||
deadline time.Time
|
||||
}
|
||||
```
|
||||
|
||||
withValue的方法实现很简单,就是创建一个valueCtx,将key和value设置到valueCtx返回。
|
||||
`timerCtx` 嵌入了 `cancelCtx`,额外增加了一个定时器。在 `WithDeadline` 中:
|
||||
|
||||
```go
|
||||
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
|
||||
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
|
||||
return WithCancel(parent) // 父 deadline 更早,直接返回 cancelCtx
|
||||
}
|
||||
c := &timerCtx{cancelCtx: newCancelCtx(parent), deadline: d}
|
||||
propagateCancel(parent, c)
|
||||
dur := time.Until(d)
|
||||
if dur <= 0 {
|
||||
c.cancel(true, DeadlineExceeded)
|
||||
return c, func() { c.cancel(false, Canceled) }
|
||||
}
|
||||
c.timer = time.AfterFunc(dur, func() {
|
||||
c.cancel(true, DeadlineExceeded)
|
||||
})
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
```
|
||||
|
||||
### 六、最佳实践
|
||||
|
||||
> [!tip] 💡 技巧
|
||||
> - **永远将 context 作为第一个参数**传入函数,命名为 `ctx`
|
||||
> - **不要将 context 存入结构体**,只作为请求级别的传递工具
|
||||
> - **不要用 context 传值做业务数据交换**,它是跨 API 边界的取消信号机制
|
||||
> - **长生命周期对象不要用 context 传值**,会导致对象无法被 GC
|
||||
|
||||
## 小结
|
||||
|
||||
- Context 是接口,四种实现各司其职:空、取消、定时、传值
|
||||
- 取消通过 `close(done channel)` + 递归遍历 children map 实现级联传播
|
||||
- 值传递通过嵌套链表实现,查找为 O(n) 线性搜索
|
||||
- `propagateCancel` 处理父子关联,支持三种场景
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hzh/GolangStar/Go语言进阶/Context]] — Context 的基础用法
|
||||
- [[hzh/GolangStar/Go语言进阶/协程池]] — Context 在 worker pool 中的应用
|
||||
- [[hzh/GolangStar/Go面试题库/Context面试题]] — Context 相关高频面试题
|
||||
|
||||
Reference in New Issue
Block a user