本文为《Java工程师转Go实战》连载第 11 篇 / 共 20 篇上一篇sync 包实战下一篇Spring Boot vs Gin类比开场Java Web 开发离不开ThreadLocal存用户 ID、租户 ID、traceIdAsync任务的取消困难重重Future.cancel(true)能打断阻塞 IO 但不一定有效Timeout注解控制超时Go 的context.Context统一解决三件事超时控制、取消传播、请求级元数据传递。它是 Go 微服务的生命线——没有 context 的 Go 代码就像没有 Spring Context 的 Java 代码一样寸步难行。一、Context 的四个核心能力typeContextinterface{// 返回截止时间如果设置了的话Deadline()(deadline time.Time,okbool)// 返回一个 channel当 context 被取消时关闭Done()-chanstruct{}// Done channel 关闭后返回取消原因Err()error// context.Canceled 或 context.DeadlineExceeded// 获取请求级元数据Value(key any)any}Context 树context.Background() ← 根节点main/init 使用 │ ├── WithValue(traceID) ← 中间件注入 traceId │ │ │ ├── WithTimeout(3s) ← API 层设置整体超时 │ │ │ │ │ ├── WithCancel() ← Service 层手动取消 │ │ │ │ │ │ │ └── DB Query继承超时取消 │ │ │ │ │ └── HTTP Call继承超时 │ │ │ └── WithDeadline(...) ← 定时任务设置截止时间 │ └── context.TODO() ← 占位符不确定该传什么时临时用核心规则取消父节点会级联取消所有子节点。子节点可以设置更短的超时但不能超过父节点。二、超时控制对标 Java TimeoutJava 做法HystrixCommand(commandProperties{HystrixProperty(nameexecution.isolation.thread.timeoutInMilliseconds,value2000)})publicUsergetUser(Longid){...}// 或者用 CompletableFutureCompletableFuture.supplyAsync(()-httpClient.get(url)).orTimeout(2,TimeUnit.SECONDS).exceptionally(ex-fallback);Go 做法func(s*UserService)GetUser(ctx context.Context,idint64)(*User,error){// 为这次调用设置 2 秒超时不影响父 context 的超时ctx,cancel:context.WithTimeout(ctx,2*time.Second)defercancel()// ⚠️ 必须 defer cancel否则泄漏 timer// HTTP 调用自动继承超时req,err:http.NewRequestWithContext(ctx,GET,fmt.Sprintf(http://user-svc/users/%d,id),nil)iferr!nil{returnnil,fmt.Errorf(new request: %w,err)}resp,err:http.DefaultClient.Do(req)iferr!nil{// 如果是超时err 会包含 context.DeadlineExceededreturnnil,fmt.Errorf(call user-svc: %w,err)}deferresp.Body.Close()varuser Useriferr:json.NewDecoder(resp.Body).Decode(user);err!nil{returnnil,fmt.Errorf(decode: %w,err)}returnuser,nil}超时在调用链中的传播// Handler整个请求 5 秒超时funcGetProfile(c*gin.Context){ctx,cancel:context.WithTimeout(c.Request.Context(),5*time.Second)defercancel()profile,err:profileService.Get(ctx,userID)// 传入 ctx// ...}// Service调用两个下游共享 5 秒预算func(s*ProfileService)Get(ctx context.Context,userIDint64)(*Profile,error){// 查用户继承父 ctx 的超时但可以设置更短的userCtx,cancel:context.WithTimeout(ctx,2*time.Second)defercancel()user,err:s.userClient.Get(userCtx,userID)iferr!nil{returnnil,err}// 查订单继承父 ctx 剩余时间orders,err:s.orderClient.ListRecent(ctx,userID)iferr!nil{returnnil,err}returnProfile{User:user,Orders:orders},nil}WithDeadline指定截止时间点// 适用于定时任务必须在某个时刻前完成deadline:time.Now().Add(30*time.Minute)ctx,cancel:context.WithDeadline(context.Background(),deadline)defercancel()// 检查剩余时间ifdl,ok:ctx.Deadline();ok{remaining:time.Until(dl)log.Info(remaining time,zap.Duration(remaining,remaining))}三、手动取消与级联传播基础取消ctx,cancel:context.WithCancel(context.Background())// 启动后台工作gofunc(){for{select{case-ctx.Done():fmt.Println(worker stopped:,ctx.Err())returndefault:doWork()}}}()// 某个条件满足时取消time.Sleep(5*time.Second)cancel()// 通知所有使用此 ctx 的 goroutine 停止级联取消的威力// 父 context 取消 → 所有子 context 自动取消funcprocessOrder(ctx context.Context,orderIDint64)error{ctx,cancel:context.WithCancel(ctx)defercancel()g,ctx:errgroup.WithContext(ctx)g.Go(func()error{returnvalidateInventory(ctx,orderID)// ctx 取消时自动中断})g.Go(func()error{returncheckPayment(ctx,orderID)// ctx 取消时自动中断})g.Go(func()error{returnreserveDelivery(ctx,orderID)// ctx 取消时自动中断})// 任一失败 → errgroup 取消 ctx → 其他 goroutine 收到取消信号returng.Wait()}Go 1.21 WithCancelCause带原因的取消ctx,cancel:context.WithCancelCause(context.Background())// 取消时带上原因cancel(fmt.Errorf(user requested stop))// 获取取消原因err:context.Cause(ctx)// user requested stop四、传值替代 ThreadLocalJava ThreadLocal 的问题// ThreadLocal 绑定到线程在线程池中容易泄漏privatestaticfinalThreadLocalStringtraceIdnewThreadLocal();// 问题 1线程池复用线程忘记 remove 就污染下一个请求// 问题 2Async 切换线程后丢失 ThreadLocal// 问题 3虚拟线程下 ThreadLocal 开销大建议迁移到 ScopedValueGo Context 传值// 定义类型安全的 key未导出类型避免 key 冲突typecontextKeyintconst(traceIDKey contextKeyiotauserIDKey tenantIDKey)// 写入funcWithTraceID(ctx context.Context,traceIDstring)context.Context{returncontext.WithValue(ctx,traceIDKey,traceID)}funcWithUserID(ctx context.Context,userIDint64)context.Context{returncontext.WithValue(ctx,userIDKey,userID)}// 读取类型安全的 getterfuncTraceIDFrom(ctx context.Context)string{v,_:ctx.Value(traceIDKey).(string)returnv}funcUserIDFrom(ctx context.Context)(int64,bool){v,ok:ctx.Value(userIDKey).(int64)returnv,ok}为什么 Context 比 ThreadLocal 更好维度Java ThreadLocalGo context.Value绑定对象线程请求context 树跨 goroutine线程切换丢失显式传递永不丢失生命周期手动 remove容易泄漏跟随 context GC可见性隐式不知道谁设了什么显式传参一目了然测试友好需要特殊处理直接构造 context 即可五、Gin 中间件完整示例链路追踪中间件funcTraceMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){// 从请求头获取或生成 traceIdtraceID:c.GetHeader(X-Trace-ID)iftraceID{traceIDuuid.New().String()}// 注入到 contextctx:WithTraceID(c.Request.Context(),traceID)c.Requestc.Request.WithContext(ctx)// 设置响应头方便客户端排查c.Header(X-Trace-ID,traceID)c.Next()}}认证中间件funcAuthMiddleware(jwtSecret[]byte)gin.HandlerFunc{returnfunc(c*gin.Context){token:c.GetHeader(Authorization)iftoken{c.AbortWithStatusJSON(401,gin.H{msg:missing token})return}claims,err:parseJWT(token,jwtSecret)iferr!nil{c.AbortWithStatusJSON(401,gin.H{msg:invalid token})return}// 注入用户信息到 contextctx:c.Request.Context()ctxWithUserID(ctx,claims.UserID)ctxWithTenantID(ctx,claims.TenantID)c.Requestc.Request.WithContext(ctx)c.Next()}}在 Service 层使用func(s*OrderService)Create(ctx context.Context,req CreateOrderReq)(*Order,error){// 从 context 取用户 ID中间件已注入userID,ok:UserIDFrom(ctx)if!ok{returnnil,ErrUnauthorized}// traceId 用于日志关联traceID:TraceIDFrom(ctx)log.Info(creating order,zap.String(traceId,traceID),zap.Int64(userId,userID),)order:Order{UserID:userID,Amount:req.Amount}// ctx 继续向下传DB 查询也能感知超时和取消iferr:s.repo.Save(ctx,order);err!nil{returnnil,fmt.Errorf(save order: %w,err)}returnorder,nil}六、Context 传值的规范与约束应该传什么✅ 适合放 context❌ 不适合放 contexttraceId / spanId业务参数userName、amountuserID / tenantID数据库连接请求来源 IP大对象文件内容认证 token已解析可选配置项实验分组标签logger 实例用 struct 字段原则// 1. 业务参数走函数参数不塞 context// ❌ctxcontext.WithValue(ctx,orderAmount,99.9)// ✅funcCreateOrder(ctx context.Context,amountfloat64)error// 2. key 用未导出类型防止包间冲突typecontextKeyint// 未导出其他包无法直接使用// 3. 提供 getter/setter 函数封装不让调用方直接操作 keyfuncWithUserID(ctx context.Context,idint64)context.Context{...}funcUserIDFrom(ctx context.Context)(int64,bool){...}// 4. Value 调用链是 O(n)——层层向上查找// 不要存太多值不要高频读取热路径中考虑其他方案七、Context 与优雅关闭HTTP 服务优雅关闭funcmain(){srv:http.Server{Addr::8080,Handler:router}// 启动服务gofunc(){iferr:srv.ListenAndServe();err!http.ErrServerClosed{log.Fatal(listen:,err)}}()// 等待退出信号quit:make(chanos.Signal,1)signal.Notify(quit,syscall.SIGINT,syscall.SIGTERM)-quit log.Info(shutting down...)// 给在途请求 30 秒完成ctx,cancel:context.WithTimeout(context.Background(),30*time.Second)defercancel()iferr:srv.Shutdown(ctx);err!nil{log.Error(server forced to shutdown,zap.Error(err))}// 关闭其他资源db.Close()log.Info(server exited)}后台任务的 Context 管理typeBackgroundWorkerstruct{ctx context.Context cancel context.CancelFunc wg sync.WaitGroup}funcNewBackgroundWorker()*BackgroundWorker{ctx,cancel:context.WithCancel(context.Background())returnBackgroundWorker{ctx:ctx,cancel:cancel}}func(w*BackgroundWorker)Start(namestring,fnfunc(context.Context)){w.wg.Add(1)gofunc(){deferw.wg.Done()log.Info(worker started,zap.String(name,name))fn(w.ctx)log.Info(worker stopped,zap.String(name,name))}()}func(w*BackgroundWorker)Stop(timeout time.Duration){w.cancel()// 通知所有 worker 停止done:make(chanstruct{})gofunc(){w.wg.Wait()close(done)}()select{case-done:log.Info(all workers stopped gracefully)case-time.After(timeout):log.Warn(workers stop timeout, force exit)}}八、常见踩坑坑说明正确做法用context.Background()跑请求处理无法取消超时形同虚设传入请求的 ctxWithValue 滥用类型不安全、难测试、性能差只传元数据业务参数走函数签名忘记defer cancel()WithTimeout/WithCancel 泄漏 goroutine 和 timer创建后立即 defer cancel()不把 ctx 传给 DB/HTTP/gRPC超时形同虚设所有 IO 操作必须接收 ctx后台任务用请求的 ctx请求结束后 ctx 取消任务中断后台任务用独立的 ctx在 ctx 中存 mutable 对象并发读写 data racectx 值应不可变示例后台任务的 ctx 问题// ❌ 请求结束后邮件可能发不出去funchandler(c*gin.Context){gosendEmail(c.Request.Context(),user.Email)// 请求返回后 ctx 取消c.JSON(200,gin.H{msg:ok})}// ✅ 后台任务用独立 context但继承 traceIdfunchandler(c*gin.Context){reqCtx:c.Request.Context()traceID:TraceIDFrom(reqCtx)bgCtx:WithTraceID(context.Background(),traceID)bgCtx,cancel:context.WithTimeout(bgCtx,30*time.Second)asyncWorker.Submit(func(){defercancel()sendEmail(bgCtx,user.Email)})c.JSON(200,gin.H{msg:ok})}九、Context 与第三方框架的集成GORM// GORM 支持 WithContextdb.WithContext(ctx).Where(id ?,id).First(user)// 超时自动取消查询ctx,cancel:context.WithTimeout(ctx,500*time.Millisecond)defercancel()err:db.WithContext(ctx).Exec(SELECT SLEEP(10)).Error// err: context deadline exceededgo-redisval,err:rdb.Get(ctx,key).Result()// 如果 ctx 超时自动中断 Redis 请求gRPC// gRPC 天然支持 context 传播// 客户端设置的超时会通过 metadata 传递给服务端ctx,cancel:context.WithTimeout(ctx,2*time.Second)defercancel()resp,err:client.GetUser(ctx,pb.GetUserReq{Id:1}) 面试追问context 能不能放全局变量不能。每个请求应有独立的 context 树请求结束即废弃。全局 context 无法取消也没有超时意义。唯一的全局是context.Background()作为根。context 底层结构是一个接口Done/Err/Deadline/Value。WithCancel返回cancelCtx含 children map 实现级联取消WithValue返回valueCtx含 key/val parent 形成链表。Value 查找是 O(n) 沿链向上。和 Java ThreadLocal 本质区别ThreadLocal 是隐式绑定线程的全局变量跨线程传递困难需要InheritableThreadLocal或TransmittableThreadLocal。Context 是显式参数传递goroutine 切换下更安全可控不会有线程池复用导致的脏数据。context.TODO() 和 context.Background() 区别语义不同但实现相同。Background()用于确定的根节点main、initTODO()用于还没想好传什么 ctx 的占位。代码审查中 TODO 应该最终被替换。WithTimeout 和 WithDeadline 区别WithTimeout(ctx, 3s)WithDeadline(ctx, time.Now().Add(3s))。Timeout 是相对时间Deadline 是绝对时间点。嵌套时取最早的截止时间。 一句话总结把 ThreadLocal 和超时取消收成 context 一个参数往下传——这不是 Go 的语法糖而是微服务架构中超时级联、取消传播、链路追踪的生命线。记住三条原则每个 IO 操作都传 ctx创建立即 defer cancel只传元数据不传业务参数。建议标签GolangcontextJavaThreadLocal微服务面试超时控制
Go 版 ThreadLocal?context.Context 超时与取消全解析
本文为《Java工程师转Go实战》连载第 11 篇 / 共 20 篇上一篇sync 包实战下一篇Spring Boot vs Gin类比开场Java Web 开发离不开ThreadLocal存用户 ID、租户 ID、traceIdAsync任务的取消困难重重Future.cancel(true)能打断阻塞 IO 但不一定有效Timeout注解控制超时Go 的context.Context统一解决三件事超时控制、取消传播、请求级元数据传递。它是 Go 微服务的生命线——没有 context 的 Go 代码就像没有 Spring Context 的 Java 代码一样寸步难行。一、Context 的四个核心能力typeContextinterface{// 返回截止时间如果设置了的话Deadline()(deadline time.Time,okbool)// 返回一个 channel当 context 被取消时关闭Done()-chanstruct{}// Done channel 关闭后返回取消原因Err()error// context.Canceled 或 context.DeadlineExceeded// 获取请求级元数据Value(key any)any}Context 树context.Background() ← 根节点main/init 使用 │ ├── WithValue(traceID) ← 中间件注入 traceId │ │ │ ├── WithTimeout(3s) ← API 层设置整体超时 │ │ │ │ │ ├── WithCancel() ← Service 层手动取消 │ │ │ │ │ │ │ └── DB Query继承超时取消 │ │ │ │ │ └── HTTP Call继承超时 │ │ │ └── WithDeadline(...) ← 定时任务设置截止时间 │ └── context.TODO() ← 占位符不确定该传什么时临时用核心规则取消父节点会级联取消所有子节点。子节点可以设置更短的超时但不能超过父节点。二、超时控制对标 Java TimeoutJava 做法HystrixCommand(commandProperties{HystrixProperty(nameexecution.isolation.thread.timeoutInMilliseconds,value2000)})publicUsergetUser(Longid){...}// 或者用 CompletableFutureCompletableFuture.supplyAsync(()-httpClient.get(url)).orTimeout(2,TimeUnit.SECONDS).exceptionally(ex-fallback);Go 做法func(s*UserService)GetUser(ctx context.Context,idint64)(*User,error){// 为这次调用设置 2 秒超时不影响父 context 的超时ctx,cancel:context.WithTimeout(ctx,2*time.Second)defercancel()// ⚠️ 必须 defer cancel否则泄漏 timer// HTTP 调用自动继承超时req,err:http.NewRequestWithContext(ctx,GET,fmt.Sprintf(http://user-svc/users/%d,id),nil)iferr!nil{returnnil,fmt.Errorf(new request: %w,err)}resp,err:http.DefaultClient.Do(req)iferr!nil{// 如果是超时err 会包含 context.DeadlineExceededreturnnil,fmt.Errorf(call user-svc: %w,err)}deferresp.Body.Close()varuser Useriferr:json.NewDecoder(resp.Body).Decode(user);err!nil{returnnil,fmt.Errorf(decode: %w,err)}returnuser,nil}超时在调用链中的传播// Handler整个请求 5 秒超时funcGetProfile(c*gin.Context){ctx,cancel:context.WithTimeout(c.Request.Context(),5*time.Second)defercancel()profile,err:profileService.Get(ctx,userID)// 传入 ctx// ...}// Service调用两个下游共享 5 秒预算func(s*ProfileService)Get(ctx context.Context,userIDint64)(*Profile,error){// 查用户继承父 ctx 的超时但可以设置更短的userCtx,cancel:context.WithTimeout(ctx,2*time.Second)defercancel()user,err:s.userClient.Get(userCtx,userID)iferr!nil{returnnil,err}// 查订单继承父 ctx 剩余时间orders,err:s.orderClient.ListRecent(ctx,userID)iferr!nil{returnnil,err}returnProfile{User:user,Orders:orders},nil}WithDeadline指定截止时间点// 适用于定时任务必须在某个时刻前完成deadline:time.Now().Add(30*time.Minute)ctx,cancel:context.WithDeadline(context.Background(),deadline)defercancel()// 检查剩余时间ifdl,ok:ctx.Deadline();ok{remaining:time.Until(dl)log.Info(remaining time,zap.Duration(remaining,remaining))}三、手动取消与级联传播基础取消ctx,cancel:context.WithCancel(context.Background())// 启动后台工作gofunc(){for{select{case-ctx.Done():fmt.Println(worker stopped:,ctx.Err())returndefault:doWork()}}}()// 某个条件满足时取消time.Sleep(5*time.Second)cancel()// 通知所有使用此 ctx 的 goroutine 停止级联取消的威力// 父 context 取消 → 所有子 context 自动取消funcprocessOrder(ctx context.Context,orderIDint64)error{ctx,cancel:context.WithCancel(ctx)defercancel()g,ctx:errgroup.WithContext(ctx)g.Go(func()error{returnvalidateInventory(ctx,orderID)// ctx 取消时自动中断})g.Go(func()error{returncheckPayment(ctx,orderID)// ctx 取消时自动中断})g.Go(func()error{returnreserveDelivery(ctx,orderID)// ctx 取消时自动中断})// 任一失败 → errgroup 取消 ctx → 其他 goroutine 收到取消信号returng.Wait()}Go 1.21 WithCancelCause带原因的取消ctx,cancel:context.WithCancelCause(context.Background())// 取消时带上原因cancel(fmt.Errorf(user requested stop))// 获取取消原因err:context.Cause(ctx)// user requested stop四、传值替代 ThreadLocalJava ThreadLocal 的问题// ThreadLocal 绑定到线程在线程池中容易泄漏privatestaticfinalThreadLocalStringtraceIdnewThreadLocal();// 问题 1线程池复用线程忘记 remove 就污染下一个请求// 问题 2Async 切换线程后丢失 ThreadLocal// 问题 3虚拟线程下 ThreadLocal 开销大建议迁移到 ScopedValueGo Context 传值// 定义类型安全的 key未导出类型避免 key 冲突typecontextKeyintconst(traceIDKey contextKeyiotauserIDKey tenantIDKey)// 写入funcWithTraceID(ctx context.Context,traceIDstring)context.Context{returncontext.WithValue(ctx,traceIDKey,traceID)}funcWithUserID(ctx context.Context,userIDint64)context.Context{returncontext.WithValue(ctx,userIDKey,userID)}// 读取类型安全的 getterfuncTraceIDFrom(ctx context.Context)string{v,_:ctx.Value(traceIDKey).(string)returnv}funcUserIDFrom(ctx context.Context)(int64,bool){v,ok:ctx.Value(userIDKey).(int64)returnv,ok}为什么 Context 比 ThreadLocal 更好维度Java ThreadLocalGo context.Value绑定对象线程请求context 树跨 goroutine线程切换丢失显式传递永不丢失生命周期手动 remove容易泄漏跟随 context GC可见性隐式不知道谁设了什么显式传参一目了然测试友好需要特殊处理直接构造 context 即可五、Gin 中间件完整示例链路追踪中间件funcTraceMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){// 从请求头获取或生成 traceIdtraceID:c.GetHeader(X-Trace-ID)iftraceID{traceIDuuid.New().String()}// 注入到 contextctx:WithTraceID(c.Request.Context(),traceID)c.Requestc.Request.WithContext(ctx)// 设置响应头方便客户端排查c.Header(X-Trace-ID,traceID)c.Next()}}认证中间件funcAuthMiddleware(jwtSecret[]byte)gin.HandlerFunc{returnfunc(c*gin.Context){token:c.GetHeader(Authorization)iftoken{c.AbortWithStatusJSON(401,gin.H{msg:missing token})return}claims,err:parseJWT(token,jwtSecret)iferr!nil{c.AbortWithStatusJSON(401,gin.H{msg:invalid token})return}// 注入用户信息到 contextctx:c.Request.Context()ctxWithUserID(ctx,claims.UserID)ctxWithTenantID(ctx,claims.TenantID)c.Requestc.Request.WithContext(ctx)c.Next()}}在 Service 层使用func(s*OrderService)Create(ctx context.Context,req CreateOrderReq)(*Order,error){// 从 context 取用户 ID中间件已注入userID,ok:UserIDFrom(ctx)if!ok{returnnil,ErrUnauthorized}// traceId 用于日志关联traceID:TraceIDFrom(ctx)log.Info(creating order,zap.String(traceId,traceID),zap.Int64(userId,userID),)order:Order{UserID:userID,Amount:req.Amount}// ctx 继续向下传DB 查询也能感知超时和取消iferr:s.repo.Save(ctx,order);err!nil{returnnil,fmt.Errorf(save order: %w,err)}returnorder,nil}六、Context 传值的规范与约束应该传什么✅ 适合放 context❌ 不适合放 contexttraceId / spanId业务参数userName、amountuserID / tenantID数据库连接请求来源 IP大对象文件内容认证 token已解析可选配置项实验分组标签logger 实例用 struct 字段原则// 1. 业务参数走函数参数不塞 context// ❌ctxcontext.WithValue(ctx,orderAmount,99.9)// ✅funcCreateOrder(ctx context.Context,amountfloat64)error// 2. key 用未导出类型防止包间冲突typecontextKeyint// 未导出其他包无法直接使用// 3. 提供 getter/setter 函数封装不让调用方直接操作 keyfuncWithUserID(ctx context.Context,idint64)context.Context{...}funcUserIDFrom(ctx context.Context)(int64,bool){...}// 4. Value 调用链是 O(n)——层层向上查找// 不要存太多值不要高频读取热路径中考虑其他方案七、Context 与优雅关闭HTTP 服务优雅关闭funcmain(){srv:http.Server{Addr::8080,Handler:router}// 启动服务gofunc(){iferr:srv.ListenAndServe();err!http.ErrServerClosed{log.Fatal(listen:,err)}}()// 等待退出信号quit:make(chanos.Signal,1)signal.Notify(quit,syscall.SIGINT,syscall.SIGTERM)-quit log.Info(shutting down...)// 给在途请求 30 秒完成ctx,cancel:context.WithTimeout(context.Background(),30*time.Second)defercancel()iferr:srv.Shutdown(ctx);err!nil{log.Error(server forced to shutdown,zap.Error(err))}// 关闭其他资源db.Close()log.Info(server exited)}后台任务的 Context 管理typeBackgroundWorkerstruct{ctx context.Context cancel context.CancelFunc wg sync.WaitGroup}funcNewBackgroundWorker()*BackgroundWorker{ctx,cancel:context.WithCancel(context.Background())returnBackgroundWorker{ctx:ctx,cancel:cancel}}func(w*BackgroundWorker)Start(namestring,fnfunc(context.Context)){w.wg.Add(1)gofunc(){deferw.wg.Done()log.Info(worker started,zap.String(name,name))fn(w.ctx)log.Info(worker stopped,zap.String(name,name))}()}func(w*BackgroundWorker)Stop(timeout time.Duration){w.cancel()// 通知所有 worker 停止done:make(chanstruct{})gofunc(){w.wg.Wait()close(done)}()select{case-done:log.Info(all workers stopped gracefully)case-time.After(timeout):log.Warn(workers stop timeout, force exit)}}八、常见踩坑坑说明正确做法用context.Background()跑请求处理无法取消超时形同虚设传入请求的 ctxWithValue 滥用类型不安全、难测试、性能差只传元数据业务参数走函数签名忘记defer cancel()WithTimeout/WithCancel 泄漏 goroutine 和 timer创建后立即 defer cancel()不把 ctx 传给 DB/HTTP/gRPC超时形同虚设所有 IO 操作必须接收 ctx后台任务用请求的 ctx请求结束后 ctx 取消任务中断后台任务用独立的 ctx在 ctx 中存 mutable 对象并发读写 data racectx 值应不可变示例后台任务的 ctx 问题// ❌ 请求结束后邮件可能发不出去funchandler(c*gin.Context){gosendEmail(c.Request.Context(),user.Email)// 请求返回后 ctx 取消c.JSON(200,gin.H{msg:ok})}// ✅ 后台任务用独立 context但继承 traceIdfunchandler(c*gin.Context){reqCtx:c.Request.Context()traceID:TraceIDFrom(reqCtx)bgCtx:WithTraceID(context.Background(),traceID)bgCtx,cancel:context.WithTimeout(bgCtx,30*time.Second)asyncWorker.Submit(func(){defercancel()sendEmail(bgCtx,user.Email)})c.JSON(200,gin.H{msg:ok})}九、Context 与第三方框架的集成GORM// GORM 支持 WithContextdb.WithContext(ctx).Where(id ?,id).First(user)// 超时自动取消查询ctx,cancel:context.WithTimeout(ctx,500*time.Millisecond)defercancel()err:db.WithContext(ctx).Exec(SELECT SLEEP(10)).Error// err: context deadline exceededgo-redisval,err:rdb.Get(ctx,key).Result()// 如果 ctx 超时自动中断 Redis 请求gRPC// gRPC 天然支持 context 传播// 客户端设置的超时会通过 metadata 传递给服务端ctx,cancel:context.WithTimeout(ctx,2*time.Second)defercancel()resp,err:client.GetUser(ctx,pb.GetUserReq{Id:1}) 面试追问context 能不能放全局变量不能。每个请求应有独立的 context 树请求结束即废弃。全局 context 无法取消也没有超时意义。唯一的全局是context.Background()作为根。context 底层结构是一个接口Done/Err/Deadline/Value。WithCancel返回cancelCtx含 children map 实现级联取消WithValue返回valueCtx含 key/val parent 形成链表。Value 查找是 O(n) 沿链向上。和 Java ThreadLocal 本质区别ThreadLocal 是隐式绑定线程的全局变量跨线程传递困难需要InheritableThreadLocal或TransmittableThreadLocal。Context 是显式参数传递goroutine 切换下更安全可控不会有线程池复用导致的脏数据。context.TODO() 和 context.Background() 区别语义不同但实现相同。Background()用于确定的根节点main、initTODO()用于还没想好传什么 ctx 的占位。代码审查中 TODO 应该最终被替换。WithTimeout 和 WithDeadline 区别WithTimeout(ctx, 3s)WithDeadline(ctx, time.Now().Add(3s))。Timeout 是相对时间Deadline 是绝对时间点。嵌套时取最早的截止时间。 一句话总结把 ThreadLocal 和超时取消收成 context 一个参数往下传——这不是 Go 的语法糖而是微服务架构中超时级联、取消传播、链路追踪的生命线。记住三条原则每个 IO 操作都传 ctx创建立即 defer cancel只传元数据不传业务参数。建议标签GolangcontextJavaThreadLocal微服务面试超时控制