首页 / Go 语言入门教程 / context 进阶与最佳实践

Go 语言入门教程

context 进阶与最佳实践

本教程共 80 篇 · 第 66 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程contextAfterFunc最佳实践并发控制取消传播

66. context 进阶与最佳实践

本节目标:掌握 Go 1.21 context 新特性 AfterFunc 和 Cause,理解 context 传播规则,避开 Value 滥用等常见坑。

context 传播规则

上一节提过「父取消,子也取消」。更完整的规则:

  • 父 context 取消,所有派生的子 context 立即取消
  • 子 context 取消,不影响父 context
  • WithTimeout 的子 context 也会派生自己的子,超时同样向下传播
Background(根,不可取消)
  ├── WithCancel A
  │     ├── WithTimeout B(2s)
  │     └── WithCancel C
  └── WithTimeout D(5s)
        └── WithCancel E

取消 A,B、C 都取消。取消 D,E 取消,但 A、B、C 不受影响。

传递 context 的习惯

Go 的惯例:需要 context 的函数,第一个参数是 ctx context.Context

func DoSomething(ctx context.Context, arg string) error {
	// ...
}

调用方传 ctx,被调方监听 ctx.Done() 或把 ctx 继续往下传。这样取消信号能从顶层一路传到底。

Note

别把 context 存到结构体字段里长期持有。它应该是请求级别的,跟着函数调用链走。存到 struct 里会让生命周期失控。

AfterFunc(Go 1.21)

Go 1.21 新增 context.AfterFunc,注册一个回调函数,在 context 被取消时自动执行:

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	stop := context.AfterFunc(ctx, func() {
		fmt.Println("context 被取消了")
	})

	go func() {
		time.Sleep(time.Second)
		cancel()
	}()

	time.Sleep(2 * time.Second)

	_ = stop // 保留引用(如需取消注册可调用 stop())
}

AfterFunc 返回一个 stop 函数,调用它可以取消注册(在 context 还没取消时):

stop := context.AfterFunc(ctx, func() {
	fmt.Println("执行清理")
})

// 如果还没取消,取消注册
if stop() {
	fmt.Println("成功取消注册")
}
Tip

AfterFunc 适合「context 取消时要做点收尾」的场景,比如关闭资源、记录日志。比自己在 goroutine 里 select 监听 Done() 更简洁。回调在新的 goroutine 里执行。

取消原因 Cause(Go 1.21)

普通 cancel() 只产生 context.Canceled 错误,看不出为什么取消。Go 1.21 提供了带原因的版本:

ctx, cancel := context.WithCancelCause(context.Background())

go func() {
	time.Sleep(time.Second)
	cancel(errors.New("用户主动断开")) // 带原因的取消
}()

<-ctx.Done()
fmt.Println(context.Cause(ctx)) // 用户主动断开
  • WithCancelCause:创建可附带原因的 context
  • WithDeadlineCause / WithTimeoutCause:超时也可附原因
  • context.Cause(ctx):获取取消原因(比 Err() 更具体)

如果没设原因,Cause 返回和 Err 一样的值。

WithoutCancel(Go 1.21)

有时你想在请求取消后还做点收尾,但不想被取消打断。WithoutCancel 返回一个不受父取消影响的 context:

func handler(ctx context.Context) {
	// 请求处理(受 ctx 取消控制)
	result := doWork(ctx)

	// 写日志(即使请求取消也要写完)
	logCtx := context.WithoutCancel(ctx)
	writeLog(logCtx, result)
}

logCtx 继承了 ctx 的值(Value),但不继承取消信号。

Value 滥用问题

WithValue 用不好会变成隐式全局变量,几个常见坑:

1. 用字符串做 key 会冲突

ctx = context.WithValue(ctx, "id", 1)
ctx = context.WithValue(ctx, "id", 2) // 覆盖了!

正确做法:用自定义类型做 key:

type ctxKey int

const (
	keyUserID ctxKey = iota
	keyTraceID
)

ctx = context.WithValue(ctx, keyUserID, 42)
ctx = context.WithValue(ctx, keyTraceID, "abc-123")

2. 把业务参数塞进去

// 坏味道:用 context 传业务数据
ctx = context.WithValue(ctx, "pageSize", 20)
ctx = context.WithValue(ctx, "query", "golang")
search(ctx)

// 应该直接传参数
search(ctx, "golang", 20)
Warning

Value 只适合传「请求级元数据」:trace ID、用户身份、认证信息、locale。业务参数老老实实用函数参数传,否则代码没法测试、没法追踪。

检查 context 是否取消

长时间运行的任务要定期检查 context,及时退出:

func process(ctx context.Context, items []Item) error {
	for _, item := range items {
		select {
		case <-ctx.Done():
			return ctx.Err() // 被取消,返回错误
		default:
		}
		// 处理 item
		if err := handle(item); err != nil {
			return err
		}
	}
	return nil
}
Tip

循环开头检查 ctx.Done()

  • 耗时 IO 操作传 ctx(http.NewRequestWithContextdb.QueryContext 等)
  • 别在循环里忘了检查,否则取消了还在空转

常见错误清单

错误后果
忘了 defer cancel()context 资源泄漏
context 存到 struct 里生命周期失控
用 string 做 Value keykey 冲突覆盖
用 Value 传业务参数代码难以维护
不检查 ctx.Done()取消了还在跑

小结

  • context 从父向子传播取消,子取消不影响父
  • 函数第一个参数传 ctx context.Context,不要存 struct
  • Go 1.21 AfterFunc:取消时自动执行回调
  • Go 1.21 WithCancelCause + Cause:带原因的取消
  • WithoutCancel:继承值但不继承取消
  • Value 只传请求级元数据,用自定义类型做 key
  • 长任务循环里定期检查 ctx.Done()

下一部分进入模块和依赖管理,学怎么管理 Go 项目的代码和依赖。