context必须显式作为第一个参数传入,否则取消、超时信号无法抵达下游;禁止用全局变量或闭包隐式传递,须通过函数参数逐层传递,并优先使用withcancel/withtimeout/withdeadline派生,withvalue仅限传递只读请求元数据。

Context 必须显式作为第一个参数传入,否则取消、超时信号根本不会抵达下游逻辑。
为什么不能用全局变量或闭包“偷藏” ctx
常见错误是定义一个包级 var globalCtx = context.Background(),或在闭包里捕获外层 ctx 后不显式传递。这会导致:子 goroutine 无法响应父请求的取消;http.Request.Context() 拿到的 deadline 信息丢失;数据库查询、HTTP 调用等阻塞操作无法被中断。
真实场景中,比如你用 database/sql 执行查询,若没把 ctx 传给 db.QueryContext(ctx, ...),哪怕 HTTP 请求已超时,SQL 查询仍会继续跑完——造成 goroutine 泄漏和资源浪费。
- 所有 I/O 函数签名应以
func(ctx context.Context, ...)开头 - 启动新 goroutine 时,必须把当前 ctx 传进去,不能在 goroutine 内部调用
context.Background() - 第三方库方法名含
Context(如QueryContext、DoContext)才支持取消,老驱动需升级
WithCancel / WithTimeout / WithDeadline 怎么选
三者都返回 (ctx context.Context, cancel func()),区别只在谁触发取消:
-
context.WithCancel(parent):由你主动调用cancel(),适合用户点击“取消上传”这类手动控制 -
context.WithTimeout(parent, 5*time.Second):底层用time.AfterFunc,5 秒后自动调用cancel();等价于WithDeadline(parent, time.Now().Add(...)) -
context.WithDeadline(parent, t):按绝对时间截止,适合对接外部系统约定的 deadline(如上游服务要求“2026-10-02T12:00:00Z 前返回”)
注意:WithTimeout 返回的 cancel 必须调用,哪怕没超时——否则 timer 不释放,长期运行会泄漏。惯用写法是 defer cancel(),但要确保 defer 在 goroutine 内部,而非外层函数中。
WithValue 只能传请求级只读元数据,key 必须防冲突
context.WithValue 不是通用状态容器,仅限传递与当前请求强相关、只读、跨 API 边界的元数据,例如:traceID、userID、authScope。
字符串 key 极易冲突(比如两个中间件都用 "user"),正确做法是定义未导出类型作 key:
type userKey struct{}
然后用 context.WithValue(ctx, userKey{}, u) 存,用 ctx.Value(userKey{}).(User) 取。这样既类型安全,又避免污染。
千万别用 WithValue 传业务参数(如分页 size、排序字段)——这些该走函数参数;也别传可变结构体,因为 Value() 返回的是 interface{},修改副本不影响原值,容易误以为“改成功了”。
HTTP handler 中如何正确接入 Context
每个 http.Request 自带 req.Context(),它已集成客户端断连、超时等信号。你只需把它一路往下传,别自己再造:
- 不要在 handler 里写
ctx := context.WithTimeout(context.Background(), ...) - 直接用
req.Context(),再派生(如加WithValue或套WithTimeout子任务) - 调用下游服务时,用
client.Do(req.WithContext(ctx))或http.NewRequestWithContext(ctx, ...) - select 等待时,始终把
和业务 channel 并列监听
最易忽略的一点:ctx.Err() 可能是 context.Canceled(客户端主动断开)或 context.DeadlineExceeded(超时),两者语义不同,日志和监控里最好区分处理。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











