Go context 工程实践:超时、取消与链路追踪
test12026-09-110 次阅读
没有 context 的 HTTP 调用迟早会拖垮服务
Go 里最容易被忽视的隐患,是 goroutine 和下游调用缺少取消信号。一个 HTTP 请求进来,里面查数据库、调第三方接口、读 Redis,只要任意一环卡住且没设超时,这个 goroutine 和它占用的连接就会一直挂着。流量一大,连接池耗尽,整个服务雪崩。context 就是用来在调用链上传递"取消"和"截止时间"的标准机制。
从请求入口带上超时
HTTP handler 拿到的 r.Context() 在客户端断开时会自动取消。我们在它上面叠加一个超时,传给所有下游调用,保证整条链路最多花 2 秒。
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
user, err := queryUser(ctx, r.URL.Query().Get("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(user)
}
下游必须消费 ctx 而不是忽略它
只传 ctx 没用,关键是每个 IO 调用都要用带 Context 的版本。下面用 QueryRowContext,数据库驱动会在 ctx 超时或取消时中断查询。
func queryUser(ctx context.Context, id string) (*User, error) {
row := db.QueryRowContext(ctx,
"SELECT id,name FROM users WHERE id=$1", id)
var u User
if err := row.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
return &u, nil
}
用值传递贯穿链路标识
context.WithValue 适合放请求级的元数据,比如 traceID。注意 key 用自己的类型,避免和其他包冲突;不要拿它传可选参数,那是反模式。
type ctxKey string
const traceKey ctxKey = "trace"
func withTrace(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, traceKey, id)
}
func traceID(ctx context.Context) string {
if v, ok := ctx.Value(traceKey).(string); ok {
return v
}
return ""
}
三个容易踩的坑
- 不要存 ctx 进结构体字段,它应作为第一个参数在函数间流动。
- cancel 函数一定要 defer 调用,否则 WithCancel/WithTimeout 创建的 goroutine 会泄漏。
- 超时时间从入口统一设,下游不要各自再套一层更长的超时,否则上层超时先触发,下层白跑。
context 不是语法糖,它是 Go 并发程序里控制资源生命周期的主动脉。把超时、取消、trace 都挂在这条链上,你的服务才能在依赖抖动时优雅降级,而不是悄悄堆积成雪崩。
T
test1
文章作者
没有 context 的 HTTP 调用迟早会拖垮服务 Go 里最容易被忽视的隐患,是 goroutine 和下游调用...
- 分类
- 技术
- 发布时间
- 2026-09-11
- 字数
- 约 1650 字
- 阅读
- 0 次
