Go 中几乎所有方法的第一个参数都是 ctx(context.Context),它的价值不是让当前函数使用,而是让整个调用链拥有统一的生命周期,并且请求元数据可以传递到每个地方。

一个典型的业务调用链

调用链中的每一层可能不会直接使用 ctx,但都会负责继续传递它。这正是 Go 社区约定俗成的规范:ctx 是调用链的"信使"。

好处一:控制请求生命周期

当请求 HTTP、数据库、Redis 时,外层超时或主动取消后,内层 IO 不应继续等待,而是立即返回取消错误。一般 http、db、redis 库都已实现好,只需传入 ctx:

// 伪代码
select {
case <-ctx.Done():
    // ctx 被取消
case res <- db.Read():
    // 等待 db 返回数据
}

好处二:跨层级传递请求元数据

请求的元信息(用户、租户、数据权限等)放入 ctx,后续每一层都可随时获取:

// service 层 或 中间件中
type userIdKey struct{}
var userIDCtxKey = userIdKey{}

// 给 ctx 加入 123
ctx = context.WithValue(ctx, userIDCtxKey, 123)

// 在后面的传递中,使用携带了元信息的 ctx

好处三:日志关联

经过前面的封装,ctx 中包含用户信息。封装打印日志的方法时,可以把相关请求信息一并打出来,让日志可关联、可检索。

好处四:链路追踪

给公司所有项目添加链路追踪时,发现没有 ctx 的 IO 工具函数无法把链路串起来。例如:

// UploadOSS 上传文件到阿里云 OSS
// file 要上传的文件路径
func UploadOSS(file string) (string, error) {
    // 通过 http 将文件上传到云
}

没有 ctx 就无法在链路中串联这条 IO,也就无法完整追踪整条请求路径。

总结

Context 最大的价值不是让当前函数使用,而是让整个调用链拥有统一的生命周期,并且请求元数据能够传递到每个地方。这正是为什么 Go 项目里几乎每个方法都带 ctx——它是调用链的统一契约。