必须用闭包工厂函数或 context.context 传递,不能导出全局变量或包级指针,因为全局变量在 go 中会导致并发竞态、无法测试、复用性差及不支持多租户;闭包工厂函数确保线程安全与可测试性,context.context 适用于请求级数据透传,且需注意键类型、值类型及生命周期管理。

必须用闭包工厂函数或 context.Context 传递,不能导出全局变量或包级指针。
为什么全局变量在 Go 模块中是危险的
Go 的包初始化是单例且跨 goroutine 共享的。一旦你定义 var ctx *core.Context 并在 init() 或 main() 中赋值,所有子包(如 token、user)都会看到同一个地址——这导致:
- 并发下无锁读写引发竞态(
go run -race会报错) - 单元测试无法 mock 或替换上下文,测试只能跑真实 DB 和日志
- 模块复用性归零:别人引入你的
github.com/you/app/token包时,必须先“设好”那个全局变量,否则 panic - 无法支持多租户或多实例场景(比如一个进程里跑两个隔离的 API 子系统)
用闭包工厂函数注入上下文(推荐用于 httprouter / net/http)
这是最轻量、最符合 Go 风格的解法。把处理器定义成「接收上下文、返回 handler」的函数,利用闭包捕获当前 *core.Context:
package token
func Create(ctx *core.Context) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// ✅ ctx 是创建时捕获的,每个路由注册独立副本,线程安全
token, err := ctx.TokenService().Generate(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte(token))
}
}
调用时直接传入运行时已初始化好的上下文:
router.POST("/token", token.Create(appCtx))
- 不依赖任何框架生命周期钩子
- 子包无需 import
main或其他高层包,依赖方向清晰 - 测试时可传入 fakeContext,完全隔离
- 注意:不要在闭包里捕获
r.Context()—— 它是每次请求才生成的,应留在 handler 内部用
用 context.Context 透传请求级数据(推荐用于中间件链路)
当你要往 handler 里塞用户身份、租户 ID、traceID 这类每次请求不同的值时,必须走 r.Context(),而不是试图“注入到模块”:
// middleware/auth.go
type userKey struct{} // 不导出类型,避免 key 冲突
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := &User{ID: 123}
ctx := context.WithValue(r.Context(), userKey{}, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
下游模块通过标准方式取值:
// handler/profile.go
func ProfileHandler(w http.ResponseWriter, r *http.Request) {
if u, ok := r.Context().Value(userKey{}).(*User); ok {
fmt.Fprintf(w, "Hello %d", u.ID)
}
}
- 键必须是自定义未导出类型(
type userKey struct{}),不能用字符串 - 取值前务必判空 + 类型断言,
if v, ok := ctx.Value(key).(T); ok { ... } - 禁止存大对象(如
*sql.DB)、禁止存可变结构体——只放请求元数据
容易被忽略的关键点
真正难的不是“怎么传”,而是“谁负责清理和生命周期对齐”:
- 闭包捕获的
*core.Context必须是应用级长生命周期对象(如启动时初始化的 config+DB+Logger 组合体),不能是 request-scoped 的临时 context - context.Context 里的值不会自动释放,如果存了大 map 或缓存句柄,可能泄漏;建议只存 string/int/bool 等基础类型
- httprouter.Handle 和 http.HandlerFunc 的签名不同,别把
func(http.ResponseWriter, *http.Request)直接塞进router.GET(..., token.Create(...))—— 类型不匹配会编译失败 - 如果你用了 Gin/Echo,它们的 context 是 wrapper,要优先用
c.Request.Context()而不是c.Context(),后者可能不含 HTTP 层信息
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











