Context - 请求生命周期管理
Context 是 Go 用于控制 Goroutine 生命周期、传递请求范围数据的标准机制。掌握 Context 是编写可取消、可超时、可追踪的并发程序的关键。
📌 核心概念
🌳
树形结构
父子 Context 级联取消
Parent → Child
⏱️
超时控制
自动取消防止阻塞
WithTimeout
📋
数据传递
请求范围键值对
WithValue
🛑
手动取消
主动通知退出
WithCancel
Context 接口定义
📖 context.Context 接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
// 方法说明:
// Deadline: 返回截止时间,ok=true 表示设置了 deadline
// Done: 返回 channel,Context 取消时关闭
// Err: 返回取消原因 (Canceled 或 DeadlineExceeded)
// Value: 获取键值对数据
💡 Context 使用原则
- 作为第一个参数:
func DoWork(ctx context.Context, ...) - 不要存储在结构体: 显式传递,保持调用链清晰
- 及时调用 cancel: 使用
defer cancel()防止资源泄漏 - 传递请求范围数据: 如 trace_id、user_id 等
- 避免滥用: 不用于传递可选参数或配置
Context 层级结构
context.Background()
├──
WithTimeout(5s)
├──
├──
WithValue("trace_id", "xxx")
└──
WithValue("user_id", "123")
WithCancel()
└──
WithDeadline(t)
HTTP Request
→
创建 Root Context
→
WithTimeout 设置超时
超时或完成
→
自动取消所有子 Context
→
释放资源
四种 Context 类型
| 类型 | 创建方法 | 取消方式 | 使用场景 |
|---|---|---|---|
| Background | context.Background() |
永不取消 | 根 Context,所有 Context 的祖先 |
| TODO | context.TODO() |
永不取消 | 不确定使用何种 Context 时的临时选择 |
| WithCancel | ctx, cancel := WithCancel(parent) |
手动调用 cancel() | 需要主动取消的场景 |
| WithTimeout | ctx, cancel := WithTimeout(parent, d) |
超时自动取消 | 防止操作无限等待 |
| WithDeadline | ctx, cancel := WithDeadline(parent, t) |
到时间自动取消 | 指定具体截止时间 |
| WithValue | ctx := WithValue(parent, key, value) |
继承父 Context | 传递请求范围数据 |
基础用法
1. WithCancel - 手动取消
📝 主动取消 Goroutine
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(time.Millisecond * 200)
}
}
}
func main() {
// 创建可取消的 Context
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保资源释放
// 启动多个 Worker
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
// 运行 1 秒后取消
time.Sleep(time.Second)
fmt.Println("Cancelling all workers...")
cancel() // 取消所有 Worker
time.Sleep(time.Millisecond * 100)
}
2. WithTimeout - 超时控制
📝 防止操作无限等待
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
// 带超时的 HTTP 请求
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
// 创建带超时的 Context
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// 创建带 Context 的请求
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func main() {
body, err := fetchWithTimeout("https://api.example.com/data", time.Second*5)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Response: %s\n", body)
}
3. WithValue - 传递数据
📝 传递请求范围数据
package main
import (
"context"
"fmt"
)
// 定义 key 类型 (避免冲突)
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
// 处理请求
func handle(ctx context.Context) {
userID := ctx.Value(userIDKey).(string)
requestID := ctx.Value(requestIDKey).(string)
fmt.Printf("Processing request %s for user %s\n", requestID, userID)
// 调用下游服务,传递 Context
callService(ctx)
}
func callService(ctx context.Context) {
// 可以从 Context 中获取上游传递的数据
requestID := ctx.Value(requestIDKey).(string)
fmt.Printf("Service called with requestID: %s\n", requestID)
}
func main() {
// 创建带数据的 Context
ctx := context.WithValue(context.Background(), userIDKey, "user-123")
ctx = context.WithValue(ctx, requestIDKey, "req-456")
handle(ctx)
}
⚠️ WithValue 注意事项
- key 使用自定义类型: 避免与其他包冲突
- 只传递请求相关数据: 不用于传递配置或可选参数
- 数据量要小: 避免在 Context 中存储大量数据
- 注意类型安全: 使用类型断言时确保类型正确
生产级模式
1. HTTP 中间件中的 Context
📝 请求追踪中间件
package main
import (
"context"
"fmt"
"net/http"
"time"
)
type contextKey string
const (
requestIDKey contextKey = "requestID"
startTimeKey contextKey = "startTime"
)
// 请求追踪中间件
func requestLogger(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 生成 Request ID
requestID := generateRequestID()
// 创建带超时和数据的 Context
ctx, cancel := context.WithTimeout(r.Context(), time.Second*30)
defer cancel()
ctx = context.WithValue(ctx, requestIDKey, requestID)
ctx = context.WithValue(ctx, startTimeKey, time.Now())
// 更新请求的 Context
r = r.WithContext(ctx)
// 调用下一个处理函数
next(w, r)
// 记录日志
startTime := ctx.Value(startTimeKey).(time.Time)
duration := time.Since(startTime)
fmt.Printf("[%s] %s %s - %v\n", requestID, r.Method, r.URL.Path, duration)
}
}
func handler(w http.ResponseWriter, r *http.Request) {
requestID := r.Context().Value(requestIDKey).(string)
fmt.Printf("Handling request %s\n", requestID)
fmt.Fprintln(w, "OK")
}
func generateRequestID() string {
return fmt.Sprintf("req-%d", time.Now().UnixNano())
}
func main() {
http.HandleFunc("/api/data", requestLogger(handler))
http.ListenAndServe(":8080", nil)
}
2. 并发任务控制
📝 使用 errgroup 管理并发任务
package main
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
// 简单实现 errgroup
type Group struct {
wg sync.WaitGroup
err error
once sync.Once
}
func (g *Group) Go(f func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := f(); err != nil {
g.once.Do(func() {
g.err = err
})
}
}()
}
func (g *Group) Wait() error {
g.wg.Wait()
return g.err
}
// 使用示例
func processWithErrGroup() error {
g := &Group{}
// 启动多个任务
for i := 0; i < 5; i++ {
i := i
g.Go(func() error {
fmt.Printf("Task %d starting\n", i)
time.Sleep(time.Millisecond * 100)
if i == 3 {
return errors.New("task 3 failed")
}
fmt.Printf("Task %d completed\n", i)
return nil
})
}
return g.Wait()
}
func main() {
if err := processWithErrGroup(); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
3. 优雅关闭
📝 服务优雅关闭
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{Addr: ":8080"}
// 启动 HTTP 服务
go func() {
fmt.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("Server error: %v\n", err)
}
}()
// 监听退出信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
fmt.Println("Shutting down server...")
// 创建带超时的 Context
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// 优雅关闭
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("Shutdown error: %v\n", err)
}
fmt.Println("Server stopped")
}
底层实现原理
Context 类型家族
📖 四种核心实现
// 1. emptyCtx - 空 Context (Background/TODO)
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 interface{}) interface{} { return nil }
// 2. cancelCtx - 可取消 Context (WithCancel)
type cancelCtx struct {
Context
mu sync.Mutex
done atomic.Value
children map[canceler]struct{}
err error
}
// 3. timerCtx - 超时 Context (WithTimeout/WithDeadline)
type timerCtx struct {
cancelCtx
timer *time.Timer // 超时自动取消
deadline time.Time
}
// 4. valueCtx - 带值 Context (WithValue)
type valueCtx struct {
Context
key, val interface{}
}
cancelCtx 详细实现
📖 WithCancel 的实现
// context/context.go - cancelCtx 结构
type cancelCtx struct {
Context
mu sync.Mutex
done atomic.Value // 懒初始化,channel 在第一次使用时创建
children map[canceler]struct{} // 子 Context 集合
err error // 取消原因
}
// Done 返回 channel (懒加载)
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
defer c.mu.Unlock()
d := c.done.Load().(chan struct{})
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d
}
// 取消操作
func (c *cancelCtx) cancel(err error, deliver bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.err != nil {
return // 已经取消
}
c.err = err
if c.done.Load() == nil {
c.done.Store(closedchan) // 使用预关闭的 channel
} else {
close(c.done.Load().(chan struct{}))
}
// 递归取消所有子 Context
for child := range c.children {
child.cancel(err, true)
}
c.children = nil
}
timerCtx 超时实现
📖 WithTimeout 的实现
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
// 如果已经超时,直接返回
if cur := time.Now(); deadline.After(cur) {
c := &timerCtx{
cancelCtx: *newCancelCtx(parent),
deadline: deadline,
}
// 设置定时器
c.timer = time.AfterFunc(deadline.Sub(cur), func() {
c.cancel(DeadlineExceeded, true)
})
return c, func() { c.cancel(Canceled, true) }
}
// 已经超时,返回已取消的 context
return &cancelCtx{err: DeadlineExceeded, done: closedchan}, func{}{}
}
// 取消时停止定时器
func (c *timerCtx) cancel(err error, deliver bool) {
c.cancelCtx.cancel(err, deliver)
if c.timer != nil {
c.timer.Stop()
}
}
valueCtx 值传递实现
📖 WithValue 的实现
type valueCtx struct {
Context
key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
return &valueCtx{parent, key, val}
}
// Value 方法沿树向上查找
func (c *valueCtx) Value(key interface{}) interface{} {
if key == c.key {
return c.val
}
return c.Context.Value(key) // 向父节点查找
}
取消传播流程图
调用 cancel()
→
设置 err
→
关闭 done channel
遍历 children
→
递归取消子 Context
→
所有子节点 Done() 返回
监听 Done() 的 Goroutine 收到信号
→
清理资源并退出
💡 Context 核心原理
- 树形传播: 父 Context 取消时,递归取消所有子节点
- 懒初始化: done channel 在第一次使用时才创建
- 原子操作: 使用 atomic.Value 保证并发安全
- 共享关闭 channel: 已取消的 Context 共享同一个 closedchan
- 无锁读取: Done() 返回的 channel 只关闭不发送,读取无锁
- Value 链式查找: 沿父节点链向上查找,O(n) 复杂度
常见陷阱与解决方案
| 陷阱 | 错误示例 | 正确做法 |
|---|---|---|
| 忘记调用 cancel | 创建后不调用 cancel | 使用 defer cancel() |
| 存储 Context 在结构体 | type S { ctx context.Context } |
作为参数显式传递 |
| 传递 nil Context | func f(ctx nil) |
使用 context.TODO() |
| 滥用 WithValue | 传递大量数据或配置 | 只传递请求范围数据 |
| 忽略 Done channel | 不检查 ctx.Done() | 在循环中使用 select |
🚨 资源泄漏示例
// ❌ 错误:忘记调用 cancel,导致资源泄漏
func leak() {
ctx, _ := context.WithTimeout(context.Background(), time.Second)
doWork(ctx) // 如果没有及时完成,goroutine 会泄漏
}
// ✅ 正确:使用 defer 确保 cancel 被调用
func noLeak() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
doWork(ctx)
}
最佳实践总结
✅ Context 使用原则
- 作为第一个参数:
func DoWork(ctx context.Context, ...) - 显式传递: 不要存储在结构体中
- 及时取消: 使用
defer cancel() - 检查 Done: 在长时间运行中使用 select 检查
- 传递必要数据: 只传递请求范围的数据
- 设置超时: 外部调用应设置合理超时