Go 的 context 是解决并发控制的工具,非语法教学;WithCancel 须由发起方调用且仅一次,避免子 goroutine 自行 cancel;WithValue 的 key 必须是私有可比较类型;HTTP 超时必须用 WithTimeout 而非 Client.Timeout;Background() 和 TODO() 语义不同但行为一致。

Go 的 context 不是用来“学语言”的,而是解决真实并发控制问题的工具——它不教语法,只管 goroutine 的生与死、快与慢、传什么和不传什么。
为什么 context.WithCancel 不能在子 goroutine 里调用
常见错误是:启动一个 goroutine 后,在里面直接调用 cancel(),以为这样能“自己退出”。结果往往出现 panic 或逻辑错乱。
- cancel 函数本质是向
ctx.Done()背后的 channel 发送关闭信号,且只能安全调用一次 - 多个 goroutine 并发调用
cancel()会触发内部sync.Mutex冲突,导致 panic(比如panic: sync: unlock of unlocked mutex) - 子 goroutine 自行 cancel 后,父流程可能还在等它完成,造成状态不一致(例如资源未释放、日志漏打)
- 正确做法:由发起方(通常是主 goroutine 或 handler)统一判断何时取消,并调用一次
cancel()
context.WithValue 传参时 key 类型必须是 unexported 类型
如果你用字符串字面量或公开变量做 key,比如 context.WithValue(ctx, "user_id", 123),看似能跑通,但极易引发键冲突和类型擦除问题。
- 标准做法是定义私有结构体或空 struct 作为 key:
type userIDKey struct{},再用context.WithValue(ctx, userIDKey{}, 123) - key 必须是可比较的(
==支持),不能是 map/slice/func 等不可比较类型 - Value 查找是线性遍历链表(从子 context 往上溯),key 类型越具体,越不容易被其他包误覆盖
- HTTP 中间件若都用
"auth"做 key,A 中间件写入、B 中间件读取,就可能读到错误值
HTTP 客户端超时必须用 context.WithTimeout,而不是 http.Client.Timeout
只设 http.Client.Timeout 无法中断 DNS 解析、TLS 握手、连接建立等阻塞阶段;只有 context 才能穿透整个请求生命周期。
立即学习“go语言免费学习笔记(深入)”;
-
http.Client.Timeout只控制从连接建立成功后到响应体读完的总耗时,对前置环节无效 -
http.NewRequestWithContext(ctx, ...)才能让 DNS 查询、TCP 连接、TLS 协商全部响应ctx.Done() - 若服务端卡在 TLS 握手(如证书校验慢),
Client.Timeout不生效,但WithTimeout会直接关闭底层 net.Conn - 注意:不要把同一个
ctx复用到多个并发请求中——一旦任一请求超时,所有共享该 ctx 的请求都会被连带取消
context.Background() 和 context.TODO() 的实际区别在哪
它们返回的都是 *emptyCtx,行为完全一致,区别只在语义和静态检查提示。
-
Background()是程序启动入口的根 context,比如main()、HTTP server 的 handler 初始化处,明确表示“这里就是上下文起点” -
TODO()是占位符,用于你暂时没想好传什么 context,但函数签名又强制要求第一个参数是ctx context.Context—— 它是给 linter(如staticcheck)看的,提示“此处需补上下文” - 线上代码中出现
TODO()通常意味着遗漏了 context 传递,CI 流程可配置规则禁止其出现在非测试代码中 - 二者都不能用来传递值(
Value()返回 nil),也不能被取消(Done()返回 nil channel)
真正难的不是记住四个方法名,而是判断某个 goroutine 是否该挂在这个 context 树上、它的取消时机是否和父节点一致、值该不该随它一起消失。这些没法靠文档背出来,得在 timeout 日志炸屏、goroutine 数持续上涨、trace ID 断裂的时候,亲手把它修回来。


















