「go-zero 里 context 怎么传?规范是啥?」—— 这篇把 ctx 扮演的角色、从哪来、该装什么不该装什么、以及一套必须遵守的传递规范一次讲清。
go-zero 严格遵守 Go 官方约定:context.Context 永远作为函数第一个参数显式传递,不存进结构体字段、不做全局变量。它承载请求级数据(userId / traceId)与取消/超时信号,和 svc(进程级依赖)各管一段。
记得住的一句话:请求级的「这是谁的请求、这次调用要多久、链路 ID 是多少」放 ctx,靠第一个参数从 handler → logic → 下游 RPC/DB 一路传;连接池、客户端这类「进程级共享资源」放 svc,二者绝不混用。
一个 context.Context 同时是三样东西:请求身份证、超时/取消开关、链路透传通道。
httpx / zrpc 默认就带超时。HTTP 入口的 ctx 来自 http.Request,框架把它塞进生成的 logic 结构;之后你只要用 l.ctx 往下传。
// 生成的 handler 里(你不用改) func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // ① 入口 ctx,来自请求 l := logic.NewLoginLogic(ctx, svcCtx) // ② 存进 logic.ctx resp, err := l.Login(&types.LoginReq{...}) httpx.OkJson(w, resp) } } // 你写的 logic 里 func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) { userId := ctxdata.Get(l.ctx, "userId") // ③ 从 ctx 取请求身份 // ④ 调用下游也带同一个 ctx —— 超时/取消/链路一起透传 u, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.GetUserReq{Id: userId}) ... }
这是最容易被搞混的地方,和 svc 的边界划清楚。
| 类别 | 放 ctx(请求级)✅ | 放 svc(进程级)✅ | 放参数 / 其他 |
|---|---|---|---|
| 用户身份 | userId、tenantId(本次请求是谁) | — | — |
| 链路 / 诊断 | traceId、spanId、自定义 metadata | — | — |
| 数据库句柄 | — | DB / Redis 连接池 | — |
| 下游客户端 | — | RPC stub / httpc | — |
| 本次请求参数 | 只想「随调用飘」的小标量可放 | — | 主请求体当显式参数传最清晰 |
| 大对象 / 可变状态 | ❌ 别放(ctx 取用有成本、易误改) | 并发安全组件(Redis/sync.Map) | 需要长期跨请求共享的放 svc |
这套规范一半来自 Go 官方、一半被 go-zero 的代码生成强制落地。
ctx 放最前:func(ctx, req)。go-zero 生成的 logic 构造器 NewXxxLogic(ctx, svcCtx) 已强制此形。l.ctx,这样超时、取消、traceId 才能一路透传下去。context.Background() 替换它,否则链路和超时全断。后台 goroutine 需要更长生命周期时,才用 context.WithoutCancel / 派生。context.WithValue(ctx, "k", v) 用裸字符串 key。定义私有类型常量 key,或用 go-zero 内置 ctxdata 包(专门取 JWT 里的字段)。l.ctx(生成器给的),但那是参数传递的产物,不是你自己 new 了存进去。从「取登录用户」到「链路透传」的落地代码。
go-zero 推荐用 jwt: Auth + ctxdata:鉴权中间件自动把 token claims 解析后写进 ctx,业务侧直接取。
// 中间件 / 或框架自带:校验后写入 ctx := ctxdata.ContextWithToken(r.Context(), token) // logic 里取(不用自己解 token) userId := ctxdata.Get(l.ctx, "userId") // 返回 any,按需转 string/int64
自己加的请求级字段,用私有类型避免 key 冲突。
type ctxKey string const tenantKey ctxKey = "tenant" // 中间件注入 ctx = context.WithValue(ctx, tenantKey, "t-001") // logic 取 tenant, _ := ctx.Value(tenantKey).(string)
只要带 l.ctx,上游配置的超时(rest/zrpc 的 Timeout)会自动生效,下游超时被取消时这里立刻返回。
// 带同一个 ctx,超时与链路自动透传 order, err := l.svcCtx.OrderRpc.GetOrder(l.ctx, &pb.GetOrderReq{Id: id}) if errors.Is(err, context.DeadlineExceeded) { return nil, errs.New(504, "下游超时") // 优雅处理取消 }
若请求结束后还想继续干活,别让 ctx 随请求取消——派生一个不被取消的 ctx。
// 否则主请求 cancel,后台 goroutine 也跟着死 bg := context.WithoutCancel(l.ctx) // 保留 value,但不继承取消 go asyncAudit(bg, orderId)
「第一个参数显式传 ctx」是 Go 官方与几乎所有 Go Web 框架的共识。
c *gin.Context / c echo.Context 既当请求载体又当数据袋,把 ctx 存成了结构体字段。go-zero 刻意不用这种做法(避免 ctx 当字段),而是把数据从 c 取出来、放进标准 context.Context 再传——更贴合官方规范。ctx context.Context,和 go-zero zrpc 完全一致;go-zero 只是把它和生成的 logic 串起来。链路透传靠 metadata,go-zero 内置帮做了。request.state / 依赖注入(Python)。Go 选择显式第一个参数,可读性更强、无隐式魔法,但写起来更啰嗦——go-zero 用 codegen 把啰嗦的部分自动化了。
· ctx = 请求身份证 + 超时/取消开关 + 链路透传通道;
· 永远第一个参数,从 handler → logic → 下游 RPC/DB 一路显式传;
· 入口 ctx 来自 r.Context(),生成器帮你存进 l.ctx,你不用自己 new;
· 身份/链路数据放 ctx,连接/客户端放 svc,二者绝不混;
· 下游调用带 l.ctx(超时/取消/链路自动透传);
· 不存结构体字段、不塞大对象/可变状态、不用裸字符串 key。