应使用 r.context() 获取真实请求上下文,而非 context.background() 或 context.todo();中间件中修改 ctx 后须通过 req = req.withcontext(newctx) 更新 request 对象;context.context 必须为函数首参;派生上下文仅限异步任务或子操作超时控制,且需 defer cancel();withvalue 的 key 应用私有类型而非 string。

HTTP handler 里怎么拿到真正的请求上下文
别写 context.Background() 或 context.TODO() —— 这俩会切断超时和取消信号,下游 goroutine 就算你写了 ctx.Done() 监听也收不到通知。
真正该用的是 r.Context(),它来自 *http.Request,从客户端发起那一刻就绑定了 deadline 和 cancel channel。
常见错误是中间件里改了 ctx 却没更新 req:必须用 req = req.WithContext(newCtx) 替换 request 对象,否则下游 handler 拿到的还是原始 ctx。
GIN/Echo 等框架封装了这层,但底层逻辑一样:c.Request.Context() 是唯一可信入口,c.Request = c.Request.WithContext(...) 才算透传成功。
为什么 Context 必须是函数第一个参数
这不是风格问题,是 Go 生态硬性约定:database/sql、net/http、grpc 所有标准库和主流框架(Gin、Chi、Echo)都要求 context.Context 为首个参数。
IDE 和 linter(比如 staticcheck)会直接报 SA1012 错误,如果位置错乱。
更重要的是,一旦塞在中间或末尾,调用链中极易漏传——比如你写 func(query string, ctx context.Context),别人调用时很容易只传 query,ctx 就被默认为 nil 或 context.Background()。
强制首参能让人一眼意识到“这个调用受上下文控制”,避免无意识绕过生命周期管理。
WithTimeout 和 WithCancel 在 handler 中怎么用才安全
多数 handler 不需要自己新建上下文;复用 r.Context() 就够了。只有两种情况才该派生:
• 启动独立于请求生命周期的 goroutine(比如异步上报日志、触发后台任务)
• 需要对某段子操作加更严格的超时(比如 DB 查询不能超过 3s,而整个请求允许 10s)
用 context.WithTimeout(r.Context(), 3*time.Second) 时,务必 defer cancel(),否则 timer 不释放,goroutine 泄漏风险极高。
慎用 context.WithCancel:它适合等待多个 RPC 并任一失败就终止其余,但绝不该在 handler 里直接调 cancel() 而不等子任务退出——容易导致资源未清理。
绝对不要跨 goroutine 复用同一个 cancel 函数,每个请求必须有自己独立的 cancel 生命周期。
WithValue 传用户信息时 key 为什么不能用 string
用 string 当 key 是高危操作,不同包里定义的 "user_id" 字符串字面量在内存里是不同地址,类型系统无法识别冲突,极易覆盖或取不到值。
正确做法是定义私有类型:type userIDKey struct{},然后 ctx = context.WithValue(ctx, userIDKey{}, 123)。
取值时必须做类型断言并判空:if id, ok := ctx.Value(userIDKey{}).(int); ok { ... },否则 panic。
别传指针或切片——context.WithValue 只适合小量、只读、不可变元数据(如 traceID、authScope);并发读写结构体字段会触发竞态,真要共享状态,用 sync.Map 或传拷贝值。
最常被忽略的点是:goroutine 启动时必须显式传入 ctx。父 goroutine 的上下文不会自动透传,go doWork() 里如果没接收 ctx 参数,那它根本不知道请求已被取消——哪怕你 upstream 已调 cancel(),它还在跑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











