子兮子兮 子兮子兮

子兮子兮风兮寒,三江七泽情洄沿。

目录
Go 服务中的 context 设计最佳实践
/        

Go 服务中的 context 设计最佳实践

context 不是用来“到处传”的,而是用来明确控制边界与生命周期的

在 Go 服务中,context.Context 几乎无处不在:
HTTP 请求、RPC 调用、数据库访问、goroutine 管理……

但在真实项目中,context 常常被误用、滥用,甚至形同虚设
这篇文章结合生产实践,系统性地整理 context 的设计原则与常见陷阱。


一、context 到底解决了什么问题?

在没有 context 之前,Go 服务中经常出现这些问题:

  • 请求已经超时,后台逻辑还在继续跑
  • 服务准备退出,goroutine 却无法被回收
  • 下游请求失败,上游却毫不知情

context 本质上解决的是 “跨层级的协作控制”,主要包含三件事:

  1. 取消信号(Cancel)
  2. 超时 / 截止时间(Timeout / Deadline)
  3. 请求级上下文数据(Value)

理解这三点,是正确使用 context 的前提。


二、context 的“边界感”非常重要

一个常见误区是:
只要有函数调用,就顺手加一个 context。

正确的边界划分

在服务型应用中,context 通常从以下位置“诞生”:

  • HTTP / gRPC 请求入口
  • 消息消费入口
  • 服务启动的 root context

例如 HTTP 服务:

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	service.DoSomething(ctx)
}

原则:

  • context 只在“请求边界”创建
  • 不要在业务深层随意 context.Background()

一旦脱离请求边界,新建的 context 就失去了意义。


三、不要把 context 当成“参数垃圾桶”

context.WithValue 是最容易被滥用的地方。

反模式示例

ctx = context.WithValue(ctx, "user", user)
ctx = context.WithValue(ctx, "token", token)
ctx = context.WithValue(ctx, "trace", traceID)

问题包括:

  • key 冲突风险
  • 类型不安全
  • 隐式依赖,阅读代码无法感知

推荐做法

1. 仅存放“请求级元数据”

  • request id
  • trace id
  • auth 信息(必要时)

2. 使用私有类型作为 key

type ctxKeyTraceID struct{}

ctx = context.WithValue(ctx, ctxKeyTraceID{}, traceID)

3. 绝不传递业务对象

context 不是 DTO,更不是 session。


四、超时应该由“最外层”决定

很多项目存在这样的代码:

func queryDB(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()
    ...
}

这种写法在底层偷偷加限制,极易引发连锁问题。

更合理的责任划分

  • 入口层:定义整体超时策略
  • 下游调用:遵循上游 context

例如:

ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()

service.Handle(ctx)

这样可以保证:

  • 超时策略集中
  • 调整行为不需要深入修改底层代码

五、所有后台 goroutine 都必须感知 context

这是 context 使用中最容易被忽略、但后果最严重的一点

错误示例

go func() {
	doWork()
}()

正确示例

go func(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
			doWork()
		}
	}
}(ctx)

原则:

启动 goroutine 的地方,必须明确它的退出条件。

否则 goroutine 就会成为生产环境中的“幽灵进程”。


六、context 不能被存储或复用

另一个常见问题是:

type Service struct {
	ctx context.Context
}

或者把 context 存进 struct / 全局变量。

为什么这是错的?

  • context 是一次请求的状态
  • 生命周期短暂
  • 不具备可复用性

正确的做法:

  • context 作为函数参数向下传递
  • 永远不要“保存 context”

七、如何在大型项目中统一 context 规范?

在多人协作项目中,建议明确以下约定:

  1. 所有对外暴露的方法,第一个参数是 context.Context
  2. 不允许在业务层新建 context.Background()
  3. 禁止在 context 中存放业务对象
  4. goroutine 必须响应 ctx.Done()
  5. 超时只在入口层控制

这些约定,比“个人经验”更重要。


八、写在最后

context 并不是 Go 的负担,而是 Go 在工程化上的一把“安全锁”。

用对它,你会发现:

  • 系统更可控
  • 资源更可回收
  • 问题更容易定位

用错它,它只会让代码变得更隐蔽、更难维护。

context 设计得好,Go 服务的下限会被显著抬高。