context.Context 是 Go 标准库里跨 API 边界传递取消信号 + 超时 + 请求值的
统一方式。所有"可能长时间运行 + 可能需要被取消"的函数都该接 ctx 作为
第一个参数。
1. 基础
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 必须 defer cancel 避免 goroutine 泄露
go doWork(ctx)
time.Sleep(2 * time.Second)
cancel() // 显式取消
cancel() 触发后所有继承这个 ctx 的下游都收到信号。
2. 超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := http.NewRequestWithContext(ctx, "GET", url, nil)
// 5 秒后自动 cancel;HTTP 客户端会立即中断请求
WithTimeout 是 WithDeadline(time.Now().Add(d)) 的简写。
3. 在函数里响应 ctx
func doWork(ctx context.Context) error {
for i := 0; i < 100; i++ {
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled 或 context.DeadlineExceeded
case <-time.After(100 * time.Millisecond):
process(i)
}
}
return nil
}
关键模式:每次能 block 的地方都 select { case <-ctx.Done(): ... }。
对 I/O 操作通常用接 ctx 的版本:
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
row := db.QueryRowContext(ctx, "SELECT ...", args...)
4. 链式派生
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 派生:更短超时
ctxShort, cancel2 := context.WithTimeout(ctx, 2*time.Second)
defer cancel2()
callRemote(ctxShort)
子 ctx 永远比父 ctx 早结束。取较短的 timeout 生效。
父 cancel → 所有子 ctx 都被 cancel。
5. ctx 传值
type traceIDKey struct{}
ctx = context.WithValue(ctx, traceIDKey{}, "abc-123")
// 下游取
tid, ok := ctx.Value(traceIDKey{}).(string)
约定:
- key 用未导出的私有 type,避免不同包冲突
- 只传"跨 API 边界的请求作用域元数据"(trace ID / user ID / locale),
不传业务参数 - 业务参数走显式函数签名
6. context.Background() vs context.TODO()
Background():根 context,main / init 用TODO():当你不知道用哪个 ctx 时用(让 linter / 自己后续修)
ctx := context.TODO() // I'll come back to wire this
7. HTTP 服务端:取请求 ctx
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // 客户端断开连接 → ctx.Done()
result, err := fetchFromDB(ctx, ...)
if err == context.Canceled {
// 客户端已经断开,没必要返回响应
return
}
json.NewEncoder(w).Encode(result)
}
客户端 close 连接时 r.Context 自动 cancel,所有下游 query 应自动中断。
省服务器 CPU / DB 连接。
8. errgroup + ctx 并发
sync/errgroup 比手写 channel + WaitGroup 简洁:
import "golang.org/x/sync/errgroup"
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, u := range urls {
i, u := i, u // capture
g.Go(func() error {
data, err := fetch(ctx, u)
if err != nil {
return err // 任一失败 ctx 自动 cancel 其它
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
errgroup.WithContext 返回的 ctx 在任一 goroutine 返回 error 时
自动 cancel。
限并发:
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // 最多 10 个 goroutine 并发
9. 不要把 ctx 存结构体
反模式:
type Server struct {
ctx context.Context // ❌
}
ctx 是请求 / 操作的属性,不是对象的属性。每次方法显式传:
type Server struct{}
func (s *Server) Handle(ctx context.Context, req Req) (Resp, error) { ... }
例外:长生命周期的"管理者"对象(如 server 自身的 shutdown ctx)
可以存——但要清楚标注。
10. cancel() 必须 defer
ctx, cancel := context.WithTimeout(..., 5*time.Second)
// 忘 defer cancel() → 即使函数正常返回,定时器还在 → goroutine leak
defer cancel()
go vet 会警告"the cancel function is not used on all paths",
认真处理。
11. ctx 的零成本约定
约定 ctx 永远作为第一个参数:
// ✅
func doWork(ctx context.Context, args Args) (Result, error)
// ❌
func doWork(args Args, ctx context.Context) (Result, error)
阅读 / IDE 补全 / linter 都基于这个约定。
12. 实际生产 pattern
func main() {
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer cancel()
srv := &http.Server{Addr: ":8080", Handler: setupHandler()}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
<-ctx.Done()
log.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Errorf("forced shutdown: %v", err)
}
}
signal.NotifyContext(Go 1.16+)一行处理信号 → ctx 模式:
- 收 SIGINT / SIGTERM → ctx.Done()
- main 等 done
- srv.Shutdown(timeout ctx) 拒新连接 + 等老请求完,30 秒超时强制退出
踩过的坑
- ctx 没传到下游:所有 hop 都要传 ctx。中间某个函数没接 → cancel 信号
断了,超时不生效。 - 用
time.Sleep(d)而不是select { case <-time.After: case <-ctx.Done():}:
cancel 后还在 sleep,不响应。 errgroup.Go里的 goroutine panic → 整个 group 崩。在 Go 函数内部
recover 自保。ctx.Value(string)用字符串作 key → 跨包冲突。永远用私有 type。
登录后参与评论。