Context必须作为函数第一个参数显式传入,不可省略或挪位;漏传会导致子goroutine脱离父上下文控制,如HTTP服务关闭时后台任务无法被取消。

Context 传递必须用函数参数显式传入
Go 的 context.Context 不是全局变量,也不能靠闭包隐式捕获——它必须作为第一个参数(或之一)显式传给每个可能需要取消、超时或携带值的函数。漏传会导致子 goroutine 完全脱离父上下文控制,比如 http.Server 关闭时,未接收 ctx 的后台任务不会被通知退出。
常见错误现象:select { case 永远不触发,因为传入的是 <code>context.Background() 或零值 context.Context{},而非上游实际派生的上下文。
- 所有 I/O 密集型函数(如数据库查询、HTTP 调用、文件读写)应接受
ctx context.Context参数 - 避免在结构体字段里存
context.Context;它不是状态,而是执行期的“指令信号” - 若已有函数签名无法改(如第三方库回调),可用
context.WithValue包装后透传,但仅限传递只读元数据,不可用于控制生命周期
WithCancel / WithTimeout / WithDeadline 的选择逻辑
三种派生方式本质都是创建可取消的子上下文,区别在于谁触发 ctx.Done() 关闭:
-
context.WithCancel(parent):由调用方手动调用返回的cancel()函数触发,适合“用户点击取消”或“任务完成主动退出”场景 -
context.WithTimeout(parent, 5*time.Second):等价于WithDeadline(parent, time.Now().Add(5*time.Second)),适合对单次操作设硬性耗时上限(如 RPC 调用) -
context.WithDeadline(parent, t):基于绝对时间点,适合长周期任务中设置最终截止时间(如批处理必须在凌晨2点前结束)
注意:WithTimeout 内部启动了定时器 goroutine,频繁创建短超时上下文(如毫秒级)会造成调度压力;高频场景建议复用 time.Timer 或改用 select + time.AfterFunc 手动控制。
立即学习“go语言免费学习笔记(深入)”;
WithValue 只能传请求级只读数据,禁止传业务对象
context.WithValue 是唯一允许往上下文塞自定义数据的方法,但它的 key 类型必须是 unexported 类型(即小写字母开头),否则极易引发 key 冲突——不同包都用 string 当 key 会互相覆盖。
典型误用:ctx = context.WithValue(ctx, "user", u) —— 这会让后续所有中间件都依赖这个 string key,且无法做类型安全检查。
- 正确做法:定义私有 key 类型,如
type userKey struct{},再用context.WithValue(ctx, userKey{}, u) - 只传轻量、请求生命周期内不变的数据,如用户 ID、请求 ID、trace ID;绝不传数据库连接、配置实例、logger 实例等可变对象
- 接收方必须用
v, ok := ctx.Value(key).(UserType)做类型断言,不能假设一定存在
HTTP Handler 中 Context 的生命周期边界很关键
Go 的 net/http 在 Go 1.7+ 自动为每个请求注入 request.Context(),但它只存活到 handler 返回或 ResponseWriter 被关闭。任何异步 goroutine(如日志上报、消息队列投递)若直接用该 ctx,很可能在 handler 结束后已失效。
常见错误现象:ctx.Err() == context.Canceled 突然出现,但业务逻辑还没跑完——因为 HTTP 连接提前断开或客户端关闭了连接。
- 需异步执行的任务,应使用
context.WithTimeout(ctx, 30*time.Second)派生新上下文,并明确设置比 handler 更宽松的超时 - 不要把
http.Request.Context()存进全局 map 或 long-lived 结构体;它的 cancel 函数会在 response 写完后自动调用 - 若需跨请求共享状态(如 session),用独立的缓存或数据库,而不是依赖 context 生命周期
真正难的不是怎么创建 context,而是判断哪个 goroutine 应该监听哪个 ctx.Done(),以及 cancel 调用时机是否与业务语义一致——比如一个上传任务,是收到完整 body 后才开始计时,还是从 Accept 就开始?这点不厘清,context 就只是个摆设。


















