别再把 Context 当塑料袋用了:8 个日常神用法

51 阅读3分钟

别再把 Context 当塑料袋用了:8 个日常神用法

一句话版:context.Context 不是“传参的塑料袋”,它是你的取消线、超时器、链路身份证。写 Go,要么不用并发;要是用了,就要把 Context 用对。


目录

  1. 先立心法:Context 的三件套
  2. 高频场景 8 例(附可直接贴的代码)
  3. 好用的小工具与模板
  4. 反模式与踩坑复盘
  5. 结语:写并发,就是写“退出路径”

1)先立心法:Context 的三件套

  • 取消(Cancel):一处取消,处处收口。任何子协程都要能在 ctx.Done() 处优雅退出。
  • 期限(Deadline/Timeout):没有超时,就等于把稳定性交给对方。
  • 值(Value):只放请求范围的只读元信息(traceID、租户、用户、locale)。不要塞业务对象、连接、可变状态。

函数签名规范:

// 1) ctx 放第一个参数;2) 不把 ctx 存到 struct;3) 禁止传 nil。
func (s *Service) DoSomething(ctx context.Context, arg Arg) (Res, error)`

2)高频场景 8 例 2.1 HTTP 中间件:给每个请求戴上“期限 + 身份证”

type ridKey struct{}

func RequestMeta(timeout time.Duration) gin.HandlerFunc {
    return func(c *gin.Context) {
        rid := c.GetHeader("X-Request-ID")
        if rid == "" { rid = uuid.NewString() }

        base := context.WithValue(c.Request.Context(), ridKey{}, rid)
        ctx, cancel := context.WithTimeout(base, timeout)
        defer cancel()

        c.Request = c.Request.WithContext(ctx)
        c.Header("X-Request-ID", rid)
        c.Next()
    }
}

func GetRID(ctx context.Context) string {
    if v, ok := ctx.Value(ridKey{}).(string); ok { return v }
    return ""
}

2.2 gRPC:自然传递超时与取消

// Client
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
resp, err := pb.NewUserClient(conn).Get(ctx, &pb.GetReq{Id: id})

// Server
func (s *Server) Get(ctx context.Context, in *pb.GetReq) (*pb.GetResp, error) {
    select {
    case <-ctx.Done():
        return nil, status.Errorf(codes.Canceled, "client canceled")
    default:
    }
}

2.3 并发批处理:errgroup.WithContext 级联取消 + 并发限流

g, ctx := errgroup.WithContext(ctx)
sem := make(chan struct{}, 32) // 并发上限

for _, u := range urls {
    u := u
    g.Go(func() error {
        select {
        case sem <- struct{}{}:
            defer func(){ <-sem }()
        case <-ctx.Done():
            return ctx.Err()
        }

        req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return err }
        defer resp.Body.Close()
        return nil
    })
}
if err := g.Wait(); err != nil {
    // 任何一个子任务失败,全体取消
}

2.4 Worker Pool:可关闭、可收缩、可取消

type Task func(context.Context) error

type Pool struct {
    tasks chan Task
    wg    sync.WaitGroup
}

func NewPool(n, q int) *Pool {
    p := &Pool{tasks: make(chan Task, q)}
    for i := 0; i < n; i++ {
        p.wg.Add(1)
        go func() {
            defer p.wg.Done()
            for t := range p.tasks {
                _ = t(context.Background())
            }
        }()
    }
    return p
}

func (p *Pool) Submit(ctx context.Context, t Task) error {
    select {
    case p.tasks <- t:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func (p *Pool) Close() { close(p.tasks); p.wg.Wait() }

最新版的go中,可以使用wg.Go()来替换执行代码,如下所示

type Task func(context.Context) error

type Pool struct {
	tasks chan Task
	wg    sync.WaitGroup
}

func NewPool(n, q int) *Pool {
	p := &Pool{tasks: make(chan Task, q)}
	for i := 0; i < n; i++ {
		p.wg.Go(func() {
			for t := range p.tasks {
				_ = t(context.Background())
			}
		})
	}
	return p
}

func (p *Pool) Submit(ctx context.Context, t Task) error {
	select {
	case p.tasks <- t:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (p *Pool) Close() { close(p.tasks); p.wg.Wait() }

2.5 服务优雅退出:一行搞定系统级 Context

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

srv := &http.Server{Addr: ":8080", Handler: r}
go func() { _ = srv.ListenAndServe() }()

<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)

2.6 数据库/缓存:所有外部访问都要带期限

ctxDB, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
defer cancel()

row := db.QueryRowContext(ctxDB, "SELECT name FROM users WHERE id=?", id)
if err := row.Scan(&name); err != nil { /* ... */ }

// Redis
res, err := rdb.Get(ctxDB, key).Result()

2.7 重试逻辑:别忘了尊重取消与超时

func Retry(ctx context.Context, max int, base time.Duration, fn func() error) error {
    var err error
    for i := 0; i < max; i++ {
        if err = fn(); err == nil { return nil }
        d := base << i
        j := time.Duration(rand.Int63n(int64(d / 2)))
        wait := d/2 + j

        select {
        case <-time.After(wait):
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    return err
}

2.8 长连接/推送(SSE/WebSocket):用 ctx.Done() 做“断线检测”

func sse(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    flusher, _ := w.(http.Flusher)

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case t := <-ticker.C:
            fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339))
            flusher.Flush()
        }
    }
}

3)好用的小工具与模板 3.1 SafeTimer:替代热路径里的 time.After

type SafeTimer struct{ t *time.Timer }

func NewSafeTimer(d time.Duration) *SafeTimer { return &SafeTimer{time.NewTimer(d)} }
func (s *SafeTimer) C() <-chan time.Time     { return s.t.C }
func (s *SafeTimer) Stop() {
    if !s.t.Stop() { select { case <-s.t.C: default: } }
}

3.2 带 ctx 的节流/限速器

type Limiter interface{ Wait(context.Context) error }

func DoWithRate(ctx context.Context, lim Limiter, fn func(context.Context) error) error {
    if err := lim.Wait(ctx); err != nil { return err }
    return fn(ctx)
}

3.3 日志联动:把 traceID 打进日志

func Info(ctx context.Context, msg string, attrs ...slog.Attr) {
    rid := GetRID(ctx)
    slog.Info(msg, append(attrs, slog.String("rid", rid))...)
}

4)反模式与踩坑复盘

❌ 把业务对象塞进 Context

❌ 把 Context 存到 struct

❌ 热路径用 time.After

❌ select { default: } 忙等

❌ 把 context.Canceled 当严重错误

❌ 协程不检查退出

❌ context.TODO() 留在生产

✅ 正确做法是:只放元信息、传而不存、ctx.Done() 收口、用 WithTimeout、用 errgroup.WithContext、日志降级。

5)结语:写并发,就是写“退出路径” Context 的价值不在“多传了一个参数”,而在你随时能让系统有尊严地停下来。当你写下 go func(){} 的那一刻,请先想好:它什么时候、在哪里、因为什么条件退出? context.Context,就是那根稳稳的“收线”。