Go context 包深度实战——超时控制、取消传播、数据传递的正确姿势
Go context 包深度实战——超时控制、取消传播、数据传递的正确姿势
适读人群:Go后端开发者、需要处理超时和请求链路的工程师 | 阅读时长:约17分钟 | 核心价值:context是Go并发和微服务的核心基础设施,用错比不用更危险
一、小周的「超时失效」线上事故
小周在一家SaaS公司做Go后端,有次上线了一个AI生成内容的功能,调用第三方AI接口。测试环境一切正常,上线后没两天,运维来报:有个接口开始堆积请求,服务内存爆了。
他赶紧看代码:
func generateContent(prompt string) (string, error) {
resp, err := http.Post(aiServiceURL, "application/json", body)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 处理响应...
return result, nil
}看起来没问题啊?
然后他发现:AI接口偶尔会响应很慢,有时候30秒、有时候60秒才返回。但他的HTTP客户端没有设置超时,http.DefaultClient 的默认超时是……没有超时。每个卡住的请求都占用一个goroutine在等待。AI接口一卡,几百个goroutine同时卡住,内存炸了。
正确方案需要用context来控制超时。
二、context的本质:请求的「身份证」
context在Go里扮演的角色是请求的上下文载体,贯穿整个请求的处理链路。它解决三类问题:
- 超时控制:整个处理链路(包括下游调用)的总超时
- 取消传播:一处取消,所有相关goroutine都收到通知
- 请求级数据传递:请求ID、用户信息等在调用链上透传
Java对比: Java没有直接等价的概念。最接近的是 ThreadLocal(传递数据)+ 显式的超时参数 + Future.cancel()(取消)三个东西组合,而且它们是互相隔离的,不能统一传递。Go的context把这三者合为一体。
三、context的四种创建方式
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 1. context.Background():根context,通常用于main函数、测试、HTTP Handler入口
ctx := context.Background()
// 2. context.TODO():还不知道用哪个context时的占位符,提示需要后续补充
// ctx := context.TODO()
// 3. WithCancel:手动取消
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel() // 一定要调用,否则goroutine/资源泄漏
// 4. WithTimeout:超时自动取消
timeoutCtx, cancel2 := context.WithTimeout(ctx, 5*time.Second)
defer cancel2()
// 5. WithDeadline:指定绝对时间取消
deadline := time.Now().Add(5 * time.Second)
deadlineCtx, cancel3 := context.WithDeadline(ctx, deadline)
defer cancel3()
// 6. WithValue:携带值(Go 1.7+)
valueCtx := context.WithValue(ctx, "requestID", "req-001")
fmt.Println("cancelCtx:", cancelCtx)
fmt.Println("timeoutCtx:", timeoutCtx)
fmt.Println("deadlineCtx:", deadlineCtx)
fmt.Println("value:", valueCtx.Value("requestID"))
}四、超时控制实战
修复小周的问题
package main
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
)
func generateContent(ctx context.Context, prompt string) (string, error) {
// 创建带context的HTTP请求
body := strings.NewReader(`{"prompt":"` + prompt + `"}`)
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://ai-service.example.com/generate", body)
if err != nil {
return "", fmt.Errorf("创建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{} // 依赖context控制超时,不设客户端超时
resp, err := client.Do(req)
if err != nil {
// context.DeadlineExceeded 或 context.Canceled
if ctx.Err() != nil {
return "", fmt.Errorf("请求被取消/超时: %w", ctx.Err())
}
return "", fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %w", err)
}
return string(data), nil
}
func handler(prompt string) (string, error) {
// HTTP handler入口:给整个请求设置10秒总超时
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := generateContent(ctx, prompt)
if err != nil {
return "", err
}
return result, nil
}
func main() {
result, err := handler("写一首关于Go语言的诗")
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", result)
}多层调用的超时继承
context的超时会自动继承:子context的超时不能超过父context。
package main
import (
"context"
"fmt"
"time"
)
func level3(ctx context.Context) error {
select {
case <-time.After(3 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func level2(ctx context.Context) error {
// 子context:2秒超时
// 但如果父context只剩1秒,实际超时是1秒
childCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return level3(childCtx)
}
func level1() error {
// 父context:1秒总超时
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
return level2(ctx)
}
func main() {
start := time.Now()
err := level1()
fmt.Printf("耗时: %v, 错误: %v\n", time.Since(start), err)
// 约1秒后超时,即使level2设了2秒
}五、取消传播实战
一处取消,全部退出
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d 收到取消信号,退出。原因: %v\n", id, ctx.Err())
return
case <-time.After(200 * time.Millisecond):
fmt.Printf("worker %d 工作中...\n", id)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
// 启动3个worker
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
time.Sleep(600 * time.Millisecond) // 让worker工作一会儿
fmt.Println("主程序:取消所有worker")
cancel() // 所有worker都会收到取消信号
wg.Wait()
fmt.Println("所有worker已退出")
}级联取消:HTTP请求链
package main
import (
"context"
"fmt"
"time"
)
// 模拟调用链:API → ServiceA → ServiceB → DB
func queryDB(ctx context.Context) error {
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("DB查询完成")
return nil
case <-ctx.Done():
fmt.Println("DB查询被取消:", ctx.Err())
return ctx.Err()
}
}
func serviceB(ctx context.Context) error {
fmt.Println("ServiceB开始处理")
return queryDB(ctx)
}
func serviceA(ctx context.Context) error {
fmt.Println("ServiceA开始处理")
// 给ServiceB设置300ms的子超时
childCtx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
defer cancel()
return serviceB(childCtx)
}
func apiHandler(totalTimeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), totalTimeout)
defer cancel()
return serviceA(ctx)
}
func main() {
fmt.Println("=== 充足时间 ===")
err := apiHandler(1 * time.Second)
fmt.Println("结果:", err) // nil
fmt.Println("=== 超时 ===")
err = apiHandler(200 * time.Millisecond) // 总超时小于DB查询时间
fmt.Println("结果:", err) // context deadline exceeded
}六、WithValue:请求数据传递
context可以携带键值对,用于在调用链上传递请求级数据(如请求ID、用户信息)。
关键规则
- 用自定义类型作为key,避免不同包之间的key冲突
- 只传递请求级数据(请求ID、认证信息、tracing),不要传业务参数
- 值应该是不可变的
package main
import (
"context"
"fmt"
)
// 用私有类型作为key,防止冲突
type contextKey string
const (
RequestIDKey contextKey = "requestID"
UserIDKey contextKey = "userID"
)
// 提供类型安全的set/get函数(推荐模式)
func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, RequestIDKey, requestID)
}
func GetRequestID(ctx context.Context) (string, bool) {
v, ok := ctx.Value(RequestIDKey).(string)
return v, ok
}
func WithUserID(ctx context.Context, userID int64) context.Context {
return context.WithValue(ctx, UserIDKey, userID)
}
func GetUserID(ctx context.Context) (int64, bool) {
v, ok := ctx.Value(UserIDKey).(int64)
return v, ok
}
// 在调用链的任意层都能取到
func businessLogic(ctx context.Context) {
requestID, _ := GetRequestID(ctx)
userID, _ := GetUserID(ctx)
fmt.Printf("[reqID=%s, userID=%d] 执行业务逻辑\n", requestID, userID)
}
func middleware(ctx context.Context) {
requestID, _ := GetRequestID(ctx)
fmt.Printf("[reqID=%s] 日志中间件记录\n", requestID)
businessLogic(ctx)
}
func main() {
// 模拟HTTP请求处理
ctx := context.Background()
ctx = WithRequestID(ctx, "req-2024-001")
ctx = WithUserID(ctx, 10086)
middleware(ctx)
}坑:不要用context传递可选函数参数
// 错误用法:把数据库连接放进context
func badUsage(ctx context.Context) {
db := ctx.Value("db").(*sql.DB) // 类型不安全,可能panic
// ...
}
// 正确:直接传参
func goodUsage(ctx context.Context, db *sql.DB) {
// ...
}七、context的三个坑
坑1:忘记调用cancel导致goroutine泄漏
package main
import (
"context"
"fmt"
"runtime"
"time"
)
// 错误:没有调用cancel
func leakCancel() {
ctx, _ := context.WithTimeout(context.Background(), time.Hour) // cancel被丢弃
go func() {
<-ctx.Done()
fmt.Println("goroutine退出")
}()
// cancel没被调用,goroutine会等1小时
}
// 正确:始终defer cancel
func noLeakCancel() {
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel() // 函数返回时取消,goroutine立即退出
go func() {
<-ctx.Done()
fmt.Println("goroutine退出")
}()
}
func main() {
fmt.Println("初始goroutine:", runtime.NumGoroutine())
leakCancel()
time.Sleep(50 * time.Millisecond)
fmt.Println("leakCancel后:", runtime.NumGoroutine()) // 多了1个泄漏的goroutine
}坑2:把context存在struct里(通常是反模式)
// 错误:context存在struct里
type BadService struct {
ctx context.Context // 不好:context应该是请求级的,不是服务级的
db *sql.DB
}
// 正确:context作为每个方法的第一个参数
type GoodService struct {
db *sql.DB
}
func (s *GoodService) GetUser(ctx context.Context, id int) (*User, error) {
// ctx是请求级的,每次调用都不同
return nil, nil
}
type User struct{ ID int }唯一例外: 实现某些接口(比如 database/sql 的某些场景)时,可能需要在struct里存储context,但这应该是接口设计决定的,不是主动选择的。
坑3:通过context传递太多数据
context不是「请求的全局状态」,它的值应该是轻量的、请求级的元数据。如果你在context里存了数据库连接、大型对象、业务参数,那就用错了。这些应该通过函数参数显式传递。
八、在Gin框架里正确使用context
package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
type contextKey string
const RequestIDKey contextKey = "requestID"
// 请求ID中间件
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = fmt.Sprintf("req-%d", time.Now().UnixNano())
}
// 把requestID注入context
ctx := context.WithValue(c.Request.Context(), RequestIDKey, requestID)
c.Request = c.Request.WithContext(ctx)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
// 超时中间件
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
func userHandler(c *gin.Context) {
ctx := c.Request.Context()
// 所有下游调用都传入ctx,自动获得超时控制和取消传播
requestID, _ := ctx.Value(RequestIDKey).(string)
fmt.Printf("[%s] 处理请求\n", requestID)
// 模拟业务处理
select {
case <-time.After(100 * time.Millisecond):
c.JSON(http.StatusOK, gin.H{"requestID": requestID, "status": "ok"})
case <-ctx.Done():
c.JSON(http.StatusGatewayTimeout, gin.H{"error": "request timeout"})
}
}
func main() {
r := gin.Default()
r.Use(RequestIDMiddleware())
r.Use(TimeoutMiddleware(5 * time.Second))
r.GET("/user/:id", userHandler)
// r.Run(":8080")
fmt.Println("服务示例代码(未启动)")
}九、总结
context的三个核心能力:
| 能力 | API | 场景 |
|---|---|---|
| 超时控制 | WithTimeout / WithDeadline | 外部调用、DB查询限时 |
| 取消传播 | WithCancel + cancel() | 客户端断开、提前终止 |
| 数据传递 | WithValue + Value() | 请求ID、认证信息透传 |
使用原则:
- context是函数的第一个参数,不存在struct里
- cancel一定要defer调用,防止资源泄漏
- 子context超时不超过父context,超时是级联的
- Value只传请求级元数据,业务参数用普通参数
小周加上context超时控制之后,AI接口慢的时候请求会在10秒内返回超时错误,goroutine数量恢复正常,内存问题彻底解决了。
