别再把 Context 当塑料袋用了:8 个日常神用法
一句话版:
context.Context不是“传参的塑料袋”,它是你的取消线、超时器、链路身份证。写 Go,要么不用并发;要是用了,就要把Context用对。
目录
- 先立心法:Context 的三件套
- 高频场景 8 例(附可直接贴的代码)
- 好用的小工具与模板
- 反模式与踩坑复盘
- 结语:写并发,就是写“退出路径”
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,就是那根稳稳的“收线”。