刚写Go那会,context对我来说就是个"到处传的参数",函数签名里写上但也不知道有什么用。
后来线上出了几次问题才明白:context是Go并发控制的灵魂。
context解决什么问题
想象一个场景:用户请求进来,你要调用3个下游服务,汇总结果返回。
go
func handleRequest(w http.ResponseWriter, r *http.Request) {
result1 := callServiceA()
result2 := callServiceB()
result3 := callServiceC()
response := merge(result1, result2, result3)
json.NewEncoder(w).Encode(response)
}
问题来了:
- 如果用户取消请求(关闭浏览器),下游调用还在跑,浪费资源
- 如果ServiceA超时了,ServiceB和C还要继续等吗?
- 请求的一些上下文信息(用户ID、TraceID)怎么传下去?
context就是解决这些问题的。
context的四种创建方式
1. context.Background()
go
ctx := context.Background()
空context,通常作为根context,在main函数或请求入口使用。
2. context.TODO()
go
ctx := context.TODO()
也是空context,用于"还不确定用什么context"的场景。语义上表示待定。
3. context.WithCancel
go
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 一定要调用,否则会泄漏
go func() {
// 做一些事情
select {
case <-ctx.Done():
fmt.Println("被取消了")
return
case <-time.After(10 * time.Second):
fmt.Println("完成")
}
}()
// 某个条件触发取消
cancel()
关键点:
- cancel()必须调用,通常defer cancel()
- 取消是传播的,父context取消,所有子context都取消
4. context.WithTimeout / context.WithDeadline
go
// 3秒超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 截止时间
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
区别:
- WithTimeout:相对时间(从现在开始多久)
- WithDeadline:绝对时间(到什么时候)
超时控制实战
HTTP请求超时
go
func fetchURL(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// 使用
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
data, err := fetchURL(ctx, "https://example.com")
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("请求超时")
} else {
fmt.Println("请求失败:", err)
}
return
}
fmt.Println(string(data))
}
数据库查询超时
go
func queryUser(ctx context.Context, db *sql.DB, userID int) (*User, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
var user User
err := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", userID).
Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("查询超时")
}
return nil, err
}
return &user, nil
}
并发请求统一超时
go
func fetchAll(ctx context.Context, urls []string) ([][]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
results := make([][]byte, len(urls))
errs := make([]error, len(urls))
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
go func(i int, url string) {
defer wg.Done()
results[i], errs[i] = fetchURL(ctx, url)
}(i, url)
}
wg.Wait()
// 检查是否超时
if ctx.Err() != nil {
return nil, ctx.Err()
}
// 检查各个请求的错误
for _, err := range errs {
if err != nil {
return nil, err
}
}
return results, nil
}
context传值
context.WithValue
go
type contextKey string
const (
userIDKey contextKey = "userID"
traceIDKey contextKey = "traceID"
)
func WithUserID(ctx context.Context, userID int) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}
func UserIDFromContext(ctx context.Context) (int, bool) {
userID, ok := ctx.Value(userIDKey).(int)
return userID, ok
}
// 使用
func handleRequest(ctx context.Context) {
ctx = WithUserID(ctx, 12345)
processOrder(ctx)
}
func processOrder(ctx context.Context) {
userID, ok := UserIDFromContext(ctx)
if !ok {
// 没有userID
return
}
fmt.Println("处理用户", userID, "的订单")
}
最佳实践:
- key用自定义类型,避免冲突
- 不要用context传业务数据,只传请求范围的元数据(TraceID、UserID等)
- 提供封装函数,不要直接暴露key
链路追踪示例
go
type contextKey string
const traceIDKey contextKey = "traceID"
func WithTraceID(ctx context.Context) context.Context {
traceID := uuid.New().String()
return context.WithValue(ctx, traceIDKey, traceID)
}
func TraceID(ctx context.Context) string {
if traceID, ok := ctx.Value(traceIDKey).(string); ok {
return traceID
}
return "unknown"
}
// 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := WithTraceID(r.Context())
log.Printf("[%s] %s %s 开始", TraceID(ctx), r.Method, r.URL.Path)
start := time.Now()
next.ServeHTTP(w, r.WithContext(ctx))
log.Printf("[%s] %s %s 完成 耗时:%v",
TraceID(ctx), r.Method, r.URL.Path, time.Since(start))
})
}
// 业务代码中使用
func queryDB(ctx context.Context) {
log.Printf("[%s] 开始查询数据库", TraceID(ctx))
// ...
}
context在各层的传递
一个典型的Web服务:
go
// Handler层
func GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userID := chi.URLParam(r, "id")
user, err := userService.GetUser(ctx, userID)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(user)
}
// Service层
func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
// 检查缓存
user, err := s.cache.Get(ctx, userID)
if err == nil {
return user, nil
}
// 查数据库
user, err = s.repo.FindByID(ctx, userID)
if err != nil {
return nil, err
}
// 写缓存
_ = s.cache.Set(ctx, userID, user)
return user, nil
}
// Repository层
func (r *UserRepository) FindByID(ctx context.Context, userID string) (*User, error) {
var user User
err := r.db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = ?", userID).
Scan(&user.ID, &user.Name, &user.Email)
return &user, err
}
// Cache层
func (c *Cache) Get(ctx context.Context, key string) (*User, error) {
val, err := c.redis.Get(ctx, key).Result()
if err != nil {
return nil, err
}
var user User
json.Unmarshal([]byte(val), &user)
return &user, nil
}
规范:context作为第一个参数,命名为ctx。
常见错误
1. 忘记cancel
go
// 错误:会泄漏
func bad() {
ctx, _ := context.WithTimeout(context.Background(), time.Second)
// ...
}
// 正确
func good() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// ...
}
2. 把context存到struct里
go
// 错误
type Service struct {
ctx context.Context // 不要这样!
}
// 正确:context作为方法参数传递
type Service struct {}
func (s *Service) DoSomething(ctx context.Context) error {
// ...
}
context是请求级别的,不应该存储。
3. 使用context.Value传业务数据
go
// 错误:不要用context传业务参数
ctx = context.WithValue(ctx, "order", order)
func processOrder(ctx context.Context) {
order := ctx.Value("order").(Order)
// ...
}
// 正确:业务数据走参数
func processOrder(ctx context.Context, order Order) {
// ...
}
4. 不检查context是否取消
go
// 错误:长循环不检查ctx
func process(ctx context.Context, items []Item) {
for _, item := range items {
doHeavyWork(item) // 即使ctx取消了也继续执行
}
}
// 正确
func process(ctx context.Context, items []Item) error {
for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
doHeavyWork(item)
}
return nil
}
context的继承关系
scss
Background (根)
│
├── WithCancel ────────────┐
│ │ │ cancel() 会取消所有子context
│ ├── WithTimeout │
│ │ │ │
│ │ └── WithValue
│ │
│ └── WithValue
│
└── WithTimeout
│
└── WithValue
特点:
- 取消向下传播,父取消,子都取消
- 取消不向上传播,子取消,父不受影响
- Value沿着链向上查找
go
func main() {
ctx1 := context.Background()
ctx2, cancel2 := context.WithCancel(ctx1)
ctx3, cancel3 := context.WithTimeout(ctx2, time.Second)
ctx4 := context.WithValue(ctx3, "key", "value")
// cancel2() 会同时取消 ctx2, ctx3, ctx4
// cancel3() 只取消 ctx3, ctx4
// ctx4.Value("key") 能取到
// ctx3.Value("key") 取不到(Value不向上传播)
defer cancel2()
defer cancel3()
}
超时时间的设计
经验值:
go
// HTTP对外接口总超时
const APITimeout = 30 * time.Second
// 单个下游服务调用
const ServiceTimeout = 5 * time.Second
// 数据库查询
const DBTimeout = 3 * time.Second
// Redis操作
const CacheTimeout = 100 * time.Millisecond
超时递减:
go
func handleRequest(ctx context.Context) {
// 请求总超时30s
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// 查缓存,100ms
data, err := queryCache(ctx)
if err == nil {
return data
}
// 查库,3s
// 注意:这里不要再创建新的timeout context
// 因为剩余时间可能不足3s了,用父context会自动继承剩余时间
data, err = queryDB(ctx)
if err != nil {
return err
}
return data
}
总结
context的三个核心功能:
- 取消信号传播:cancel()
- 超时控制:WithTimeout/WithDeadline
- 请求级数据传递:WithValue
使用规范:
- context作为第一个参数
- 不要存到struct里
- cancel必须调用
- WithValue只传元数据,不传业务数据
- 长操作要检查ctx.Done()
记住一句话:context是请求的生命周期控制器。
有问题评论区聊。