应使用私有未导出类型作 key(如 type userKey struct{}),避免用字符串等基础类型,防止冲突和类型不安全;HTTP 中间件中须在每个请求入口新建 context 并调用 r.WithContext() 更新,提取时需安全类型断言。

为什么 context.WithValue 容易引发 panic 或数据丢失
直接传 nil、传非导出类型键、或在中间件里覆盖已有键,都会导致下游取不到值甚至 panic。最常见的是用字符串字面量当 key:ctx = context.WithValue(ctx, "user_id", 123) —— 这会和其它包的同名字符串 key 冲突,且 Go 类型系统完全无法检查。
安全做法是定义私有未导出的类型作为 key:
type userKey struct{}
然后用该类型的零值作 key:ctx = context.WithValue(ctx, userKey{}, &User{ID: 123})。这样既避免冲突,又能让编译器帮你检查类型。
- 永远不用
string、int等基础类型做 key - key 类型不导出(首字母小写),防止外部包误用
- 值类型尽量用指针或不可变结构体,避免意外修改
如何在 HTTP 中间件里正确注入和提取 value
Go 的 http.Handler 链天然适合用 context 传递请求级数据,但必须注意生命周期:每个请求应有独立 context,且不能复用或缓存。
典型错误是在 handler 外部提前创建带 value 的 context 并复用 —— 这会导致并发请求互相污染。
正确方式是在每个请求的 handler 入口新建并注入:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := extractUserID(r.Header.Get("X-User-ID"))
ctx := context.WithValue(r.Context(), userKey{}, userID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
- 务必调用
r.WithContext()替换 request 的 context,不能只改局部变量 - 提取时用相同 key 类型:
userID := r.Context().Value(userKey{}).(int) - 类型断言前建议先判断是否为
nil,避免 panic
为什么不要用 context.WithValue 传业务参数
比如把数据库连接、配置对象、日志实例塞进 context —— 这违反了 context 的设计本意:它只用于跨 API 边界的**请求范围元数据**(如 trace ID、用户身份、超时控制),不是通用依赖注入容器。
后果明显:
- 函数签名隐藏依赖,难以测试和 mock
- 静态分析失效,IDE 找不到哪些函数用了哪个 value
- 一旦 key 类型变更,所有断言处都得手动改,无编译提示
- 性能上多一层 map 查找,虽小但不必要
替代方案更清晰:显式传参,或用 struct 封装依赖(如 handler{db *sql.DB, logger *zap.Logger})。
提取 value 时最容易忽略的类型安全检查
很多人写 v := ctx.Value(myKey{}).(MyType),一旦值不存在或类型不符就 panic。Go 不会在编译期报错,运行时才崩。
必须加安全断言:
if v, ok := ctx.Value(userKey{}).(int); ok {
// 使用 v
} else {
// 处理缺失或类型错误,比如返回 401 或 log.Warn
}
更进一步,可封装成工具函数:
func UserIDFromCtx(ctx context.Context) (int, bool) {
v, ok := ctx.Value(userKey{}).(int)
return v, ok
}
这样调用侧逻辑干净,且所有提取点都统一了行为。
真正麻烦的不是写法,而是团队里有人图省事用字符串当 key,或者在 goroutine 里误传了父 context 而不是派生 context —— 这类问题不会报错,只会让某些请求静默丢失数据。上线前最好用静态检查工具(如 go vet -shadow 配合自定义 analyzer)扫一遍 key 使用模式。

















