vault backup: 2026-06-07 12:14:39
This commit is contained in:
+152
-225
@@ -1,267 +1,194 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go进阶
|
||||
- context
|
||||
- 并发
|
||||
tags: [go, golang, Context, 并发, 超时控制]
|
||||
create time: 2026-06-07 14:55
|
||||
---
|
||||
|
||||
# Context
|
||||
|
||||
## Context是什么
|
||||
`context`是Go语言在1.7版本中引入的一个标准库接口,其定义如下:
|
||||
## 概述
|
||||
|
||||
Context 是 Go 中跨 goroutine 传递取消信号、截止时间、请求作用域值的标准机制。它是构建可取消、可超时、可追踪的并发程序的核心工具。
|
||||
|
||||
## 正文
|
||||
|
||||
### Context 接口
|
||||
|
||||
```go
|
||||
type Context interface {
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
Done() <-chan struct{}
|
||||
Err() error
|
||||
Value(key interface{}) interface{}
|
||||
Deadline() (deadline time.Time, ok bool) // 截止时间
|
||||
Done() <-chan struct{} // 取消信号通道
|
||||
Err() error // 取消原因
|
||||
Value(key interface{}) interface{} // 键值对存储
|
||||
}
|
||||
```
|
||||
这个接口定义了四个方法:
|
||||
- `Deadline`: 设置 `context.Context` 被取消的时间,即截止时间
|
||||
- `Done`: 返回一个只读`Channel`,当Context被取消或者到达截止时间,这个`Channel`就会被关闭,表示Context的链路结束,多次调用`Done`方法会返回同一个`Channel`
|
||||
- `Err`: 返回`context.Context`结束的原因,它只会在`Done`返回的`Channel`被关闭时才会返回非空的值,返回值有以下两种情况:
|
||||
- 如果是`context.Context`被取消,返回`Canceled`
|
||||
- 如果是`context.Context`超时,返回`DeadlineExceeded`
|
||||
- `Value`: 从`context.Context`中获取键对应的值,类似于map的get方法,对于同一个context,多次调用`Value`并传入相同的Key会返回相同的结果,如果没有对应的key,则返回nil,键值对是通过`WithValue`方法写入的
|
||||
|
||||
## Context创建
|
||||
### 根Context创建
|
||||
主要有以下两种方式创建根context:
|
||||
> [!question] 💭 思考
|
||||
> 当一个 HTTP 请求被客户端断开连接时,如何通知所有正在处理的 goroutine 立即停止?
|
||||
|
||||
四个方法各司其职:
|
||||
- `Deadline`:返回截止时间(如果设置了)
|
||||
- `Done`:返回只读 channel,关闭时表示应停止工作
|
||||
- `Err`:返回取消原因(`context.Canceled` 或 `context.DeadlineExceeded`)
|
||||
- `Value`:获取请求级别的上下文信息(如 traceID、用户信息)
|
||||
|
||||
### 创建根 Context
|
||||
|
||||
```go
|
||||
context.Background()
|
||||
context.TODO()
|
||||
ctx := context.Background() // 空 context,不可取消,作为所有 context 的祖先
|
||||
ctx := context.TODO() // 不确定该用哪个 context 时的占位
|
||||
```
|
||||
|
||||
从源代码分析`context.Background`和`context.TODO`并没有太多的区别,都是用于创建根context,根context是一个空的context,不具备任何功能。但是一般情况下,如果当前函数没有上下文作为入参,我们都会使用`context.Background`创建一个根context作为起始的上下文向下传递。
|
||||
> [!note] 📝 Background vs TODO
|
||||
> - `Background()`:已知没有父 context 时使用(如 main 函数入口)
|
||||
> - `TODO()`:暂时不知道用什么 context,后续会替换——相当于"先写代码,稍后完善"
|
||||
|
||||
### Context派生
|
||||
根`context`在创建之后,不具备任何的功能,为了让`context`在我们的程序中发挥作用,我们要依靠`context`包提供的`With`系列函数来进行派生。
|
||||
主要有以下几个派生函数:
|
||||
```go
|
||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
|
||||
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
|
||||
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
|
||||
func WithValue(parent Context, key, val interface{}) Context
|
||||
### Context 派生树
|
||||
|
||||
> [!info] ℹ️ Context 的传播链
|
||||
> Context 通过 With 系列函数层层派生,形成一棵树。取消一个父 context,所有子 context 都会收到取消信号。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
BG["background"] --> C1["WithCancel"]
|
||||
C1 --> C2["WithTimeout"]
|
||||
C1 --> C3["WithValue"]
|
||||
C2 --> C4["WithValue"]
|
||||
|
||||
style BG fill:#e8f5e9
|
||||
style C1 fill:#fff3e0
|
||||
style C2 fill:#e3f2fd
|
||||
style C3 fill:#fce4ec
|
||||
style C4 fill:#f3e5f5
|
||||
```
|
||||
基于当前`context`,每个`with`函数都会创建出一个新的`context`,这样类似于我们熟悉的树结构,当前`context`称为父`context`,派生出的新`context`称为子`context`。就像下面的`context`树结构:
|
||||
|
||||

|
||||
| 派生函数 | 功能 | 典型场景 |
|
||||
|----------|------|---------|
|
||||
| `WithCancel` | 手动取消 | 业务逻辑主动终止 |
|
||||
| `WithTimeout` | 超时自动取消 | RPC 调用、HTTP 请求 |
|
||||
| `WithDeadline` | 指定时刻取消 | 定时任务截止 |
|
||||
| `WithValue` | 传递请求级数据 | traceID、用户认证信息 |
|
||||
|
||||
通过根`context`,通过四个`with`系列方法可以派生出四种类型的`context`,每种`context`又可以通过同样的方式调用`with`系列方法继续向下派生新的`context`,整个结构像一棵树。
|
||||
### 并发控制模式
|
||||
|
||||
## Context有什么用
|
||||
`context`主要有两个用途,也是在项目中经常使用的:
|
||||
1. 用于并发控制,控制协程的优雅退出
|
||||
2. 上下文的信息传递
|
||||
总的来说,`context`就是用来在父子`goroutine`间进行值传递以及发送`cancel`信号的一种机制。
|
||||
#### WithCancel — 手动取消
|
||||
|
||||
### 并发控制
|
||||
对于一般的服务器而言,都是一致运行着的,等待接收来自客户端或者浏览器的请求做出响应,思考这样一种场景,后台微服务架构中,一般服务器在收到一个请求之后,如果逻辑复杂,不会在一个`goroutine`中完成,而是会创建出很多的`goroutine`共同完成这个请求,就像下面这种情况:
|
||||
|
||||

|
||||
|
||||
有一个请求过来之后,先经过第一次`rpc`调用,然后再到`rpc2`,后面创建执行两个`rpc`,`rpc4`里又有一次`rpc`调用`rpc5`,等所有`rpc`调用成功后,返回结果。假如在整个调用过程中,`rpc1`发生了错误,如果没有`context`存在的话,我们还是得等所有的`rpc`都执行完才能返回结果,这样其实浪费了不少时间,因为一旦出错,我们完全可以直接在`rpc1`这里就返回结果了,不用等到后续的`rpc`都执行完。
|
||||
假设我们在`rpc1`直接返回失败,不等后续的`rpc`继续执行,那么其实后续的`rpc`执行就是没有意义的,浪费计算和IO资源而已。引入`context`之后,就可以很好的处理这个问题,在不需要子`goroutine`执行的时候,可以通过`context`通知子`goroutine`优雅的关闭。
|
||||
|
||||
#### context.WithCancel
|
||||
方法定义如下:
|
||||
```go
|
||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
|
||||
```
|
||||
`context.WithCancel`函数是一个取消控制函数,只需要一个`context`作为参数,能够从`context.Context`中衍生出一个新的子`context`和取消函数`CancelFunc`,通过将这个子`context`传递到新的`goroutine`中来控制这些`goroutine`的关闭,一旦我们执行返回的取消函数`CancelFunc`,当前上下文以及它的子上下文都会被取消,所有的`Goroutine`都会同步收到取消信号。
|
||||
使用示例:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go Watch(ctx, "goroutine1")
|
||||
go Watch(ctx, "goroutine2")
|
||||
|
||||
time.Sleep(6 * time.Second) // 让goroutine1和goroutine2执行6s
|
||||
fmt.Println("end watching!!!")
|
||||
cancel() // 通知goroutine1和goroutine2关闭
|
||||
time.Sleep(1 * time.Second)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go watch(ctx, "observer1")
|
||||
go watch(ctx, "observer2")
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
cancel() // 通知所有监听者退出
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
func Watch(ctx context.Context, name string) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Printf("%s exit!\n", name) // 主goroutine调用cancel后,会发送一个信号到ctx.Done()这个channel,这里就会收到信息
|
||||
return
|
||||
default:
|
||||
fmt.Printf("%s watching...\n", name)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
func watch(ctx context.Context, name string) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Printf("%s: %v\n", name, ctx.Err())
|
||||
return
|
||||
default:
|
||||
fmt.Printf("%s working...\n", name)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
goroutine2 watching...
|
||||
goroutine1 watching...
|
||||
goroutine1 watching...
|
||||
goroutine2 watching...
|
||||
goroutine2 watching...
|
||||
goroutine1 watching...
|
||||
goroutine1 watching...
|
||||
goroutine2 watching...
|
||||
goroutine2 watching...
|
||||
goroutine1 watching...
|
||||
goroutine1 watching...
|
||||
goroutine2 watching...
|
||||
end watching!!!
|
||||
goroutine1 exit!
|
||||
goroutine2 exit!
|
||||
```
|
||||
`ctx, cancel := context.WithCancel(context.Background())`派生出了一个带有返回函数`cancel`的`ctx`,并把它传入到子`goroutine`中,接下来在6s时间内,由于没有执行`cancel`函数,子`goroutine`将一直执行`default`语句,打印监控。6s之后,调用`cancel`,此时子`goroutine`会从`ctx.Done()`这个`channel`中收到消息,执行`return`结束。
|
||||
|
||||
#### context.WithDeadline
|
||||
方法定义如下:
|
||||
> [!tip] 💡 必记习惯:cancel 函数要调用
|
||||
> ```go
|
||||
> ctx, cancel := context.WithTimeout(parent, 5*time.Second)
|
||||
> defer cancel() // 延迟释放资源,即使提前返回也不会泄漏
|
||||
> ```
|
||||
|
||||
#### WithTimeout / WithDeadline
|
||||
|
||||
```go
|
||||
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
|
||||
// 超时 5 秒
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 截止到明天中午 12 点
|
||||
ctx, cancel := context.WithDeadline(context.Background(), tomorrowNoon)
|
||||
defer cancel()
|
||||
```
|
||||
`context.WithDeadline`也是一个取消控制函数,方法有两个参数,第一个参数是一个`context`,第二个参数是截止时间,同样会返回一个子`context`和一个取消函数`CancelFunc`。在使用的时候,没有到截止时间,我们可以通过手动调用`CancelFunc`来取消子`context`,控制子`goroutine`的退出,如果到了截止时间,我们都没有调用`CancelFunc`,子`context`的`Done()`管道也会收到一个取消信号,用来控制子`goroutine`退出。
|
||||
使用示例:
|
||||
|
||||
> [!note] 📝 Timeout vs Deadline
|
||||
> - `WithTimeout(d)` = 从现在开始经过 d 时间后超时
|
||||
> - `WithDeadline(t)` = 在绝对时间点 t 超时
|
||||
> - 大多数情况下使用 `WithTimeout` 更直观
|
||||
|
||||
#### WithValue — 传递请求级数据
|
||||
|
||||
```go
|
||||
package main
|
||||
type contextKey string
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
const userIDKey contextKey = "userID"
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithDeadline(context.Background(),time.Now().Add(4*time.Second)) // 设置超时时间4当前时间4s之后
|
||||
defer cancel()
|
||||
go Watch(ctx, "goroutine1")
|
||||
go Watch(ctx, "goroutine2")
|
||||
|
||||
time.Sleep(6 * time.Second) // 让goroutine1和goroutine2执行6s
|
||||
fmt.Println("end watching!!!")
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// 从请求头获取用户 ID,放入 context
|
||||
ctx := context.WithValue(r.Context(), userIDKey, getUserID(r))
|
||||
|
||||
// 传递给下游 goroutine
|
||||
processRequest(ctx)
|
||||
}
|
||||
|
||||
func Watch(ctx context.Context, name string) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Printf("%s exit!\n", name) // 4s之后收到信号
|
||||
return
|
||||
default:
|
||||
fmt.Printf("%s watching...\n", name)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
func processRequest(ctx context.Context) {
|
||||
if id := ctx.Value(userIDKey); id != nil {
|
||||
log.Printf("processing for user %v", id)
|
||||
}
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
goroutine1 watching...
|
||||
goroutine2 watching...
|
||||
goroutine2 watching...
|
||||
goroutine1 watching...
|
||||
goroutine1 watching...
|
||||
goroutine2 watching...
|
||||
goroutine1 exit!
|
||||
goroutine2 exit!
|
||||
end watching!!!
|
||||
```
|
||||
我们并没有调用`cancel`函数,但是在过了4s之后,子`groutine`里`ctx.Done()`收到了信号,打印出`exit`,子`goroutine`退出,这就是`WithDeadline`派生子`context`的用法。
|
||||
|
||||
#### context.WithTimeout
|
||||
方法定义:
|
||||
> [!warning] ⚠️ Value 的使用禁忌
|
||||
> 1. **不要用 Context 传递可选参数**——那是函数参数的职责
|
||||
> 2. **key 不要使用 string/int 类型**——容易冲突,自定义 type 作为 key
|
||||
> 3. **Value 是不可变的**——子 context 只能读取不能修改父的值
|
||||
> 4. **不用于传递业务核心数据**——这些数据应该作为函数参数显式传递
|
||||
|
||||
### 完整实战:带超时的 RPC 链路
|
||||
|
||||
```go
|
||||
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
|
||||
```
|
||||
`context.WithTimeout`和`context.WithDeadline`的作用类似,都是用于超时取消子`context`,只是传递的第二个参数有所不同,`context.WithTimeout`传递的第二个参数不是具体时间,而是时间长度。
|
||||
使用示例:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||||
defer cancel()
|
||||
go Watch(ctx, "goroutine1")
|
||||
go Watch(ctx, "goroutine2")
|
||||
|
||||
time.Sleep(6 * time.Second) // 让goroutine1和goroutine2执行6s
|
||||
fmt.Println("end watching!!!")
|
||||
}
|
||||
|
||||
func Watch(ctx context.Context, name string) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Printf("%s exit!\n", name) // 主goroutine调用cancel后,会发送一个信号到ctx.Done()这个channel,这里就会收到信息
|
||||
return
|
||||
default:
|
||||
fmt.Printf("%s watching...\n", name)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. 基于请求创建 context(携带超时和 traceID)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ctx = context.WithValue(ctx, traceIDKey, generateTraceID())
|
||||
|
||||
// 2. 并行调用多个下游服务
|
||||
resultCh := make(chan Result, 2)
|
||||
|
||||
go func() {
|
||||
resultCh <- callServiceA(ctx)
|
||||
}()
|
||||
go func() {
|
||||
resultCh <- callServiceB(ctx)
|
||||
}()
|
||||
|
||||
// 3. 等待结果或超时
|
||||
select {
|
||||
case r1 := <-resultCh:
|
||||
handleResult(r1)
|
||||
case r2 := <-resultCh:
|
||||
handleResult(r2)
|
||||
case <-ctx.Done():
|
||||
http.Error(w, "request timeout", http.StatusGatewayTimeout)
|
||||
}
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
goroutine2 watching...
|
||||
goroutine1 watching...
|
||||
goroutine1 watching...
|
||||
goroutine2 watching...
|
||||
goroutine2 watching...
|
||||
goroutine1 watching...
|
||||
goroutine1 watching...
|
||||
goroutine2 watching...
|
||||
goroutine1 exit!
|
||||
goroutine2 exit!
|
||||
end watching!!!
|
||||
```
|
||||
程序很简单,与上个`context.WithDeadline`的样例代码基本一样,只是改变了下派生`context`的方法为`context.WithTimeout`,具体体现在第二个参数不再是具体时间,而是变为了4s这个具体的时间长度,执行结果也是一样。
|
||||
|
||||
#### context.WithValue
|
||||
方法定义:
|
||||
```go
|
||||
func WithValue(parent Context, key, val interface{}) Context
|
||||
```
|
||||
`context.WithValue`函数从父`context`中创建一个子`context`用于传值,函数参数是父`context`,`key`,`val`键值对。返回一个`context`。
|
||||
项目中这个方法一般用于上下文信息的传递,比如请求唯一`id`,以及`trace_id`等,用于链路追踪以及配置透传。
|
||||
使用示例:
|
||||
```go
|
||||
package main
|
||||
> [!warning] ⚠️ Context 传播的最佳实践
|
||||
> - Context 必须作为函数的**第一个参数**,命名为 `ctx`
|
||||
> - 永远用传入的 context 创建新的 context,不要自己新建 background
|
||||
> - 所有网络调用、DB 查询都应接受 context 并检查取消信号
|
||||
> - 不要在 goroutine 中忽略 context.Done()——否则会导致 goroutine leak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
## 关联笔记
|
||||
|
||||
func func1(ctx context.Context) {
|
||||
fmt.Printf("name is: %s", ctx.Value("name").(string))
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.WithValue(context.Background(), "name", "zhangsan")
|
||||
go func1(ctx)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
name is: zhangsan
|
||||
```
|
||||
- [[hzh/GolangStar/Go语言进阶/Select]]
|
||||
- [[hzh/GolangStar/Go语言进阶/Goroutine]]
|
||||
- [[hzh/GolangStar/Go语言原理/context原理]]
|
||||
|
||||
Reference in New Issue
Block a user