context超时传播的常见陷阱
Go语言的context包是并发控制的核心基础设施,但context超时传播在实际使用中存在多个易错点。最常见的问题是:父context取消后,使用该context创建的子goroutine如果没有正确检查ctx.Done(),将无限挂起,造成goroutine泄漏。
错误示例:
func fetchData(ctx context.Context) ([]byte, error) {
ch := make(chan []byte, 1)
go func() {
data, _ := slowAPICall() // 未传入ctx,无法被取消
ch <- data
}()
select {
case data := <-ch:
return data, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
当ctx超时退出时,内部goroutine的 slowAPICall() 仍在运行,且channel写入也会阻塞(ch容量为1,但只有一条数据)。修正方法是将ctx传递到子goroutine,并确保子goroutine有退出路径。
正确模式:可取消的子goroutine
func fetchData(ctx context.Context) ([]byte, error) {
ch := make(chan result, 1)
go func() {
data, err := slowAPICall(ctx) // 传入ctx
ch <- result{data, err}
}()
select {
case r := <-ch:
return r.data, r.err
case <-ctx.Done():
return nil, ctx.Err()
}
}
type result struct {
data []byte
err error
}
关键点:channel容量为1且使用struct包装结果,即使父goroutine因超时退出select,子goroutine写入channel也不会阻塞——写入成功后goroutine正常退出。
context嵌套与超时覆盖规则
context的超时传播遵循"最短超时优先"原则:
func handler(ctx context.Context) {
// 父context超时5秒
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// 子context超时3秒——实际超时取min(剩余5s, 3s) = 3秒
childCtx, childCancel := context.WithTimeout(ctx, 3*time.Second)
defer childCancel()
// 子context超时10秒——实际超时取min(剩余5s, 10s) = 5秒
// 父context先超时,子context随之取消
longCtx, longCancel := context.WithTimeout(ctx, 10*time.Second)
defer longCancel()
}
子context的超时不能超过父context的剩余时间。这是设计时容易忽略的:设置一个看似很长的子超时,实际上被父context提前截断。在微服务调用链中,网关层设置的全局超时会自动传播到下游每个服务。
HTTP客户端请求超时与context联动
Go标准库的http.Client天然支持context取消:
func (s *Service) CallAPI(ctx context.Context, url string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// Client级别的Timeout会覆盖context超时
client := &http.Client{
Timeout: 30 * time.Second, // 最大超时
}
resp, err := client.Do(req)
if err != nil {
if ctx.Err() != nil {
return nil, fmt.Errorf("context canceled: %w", ctx.Err())
}
return nil, err
}
return resp, nil
}
注意:当Client.Timeout和context.Deadline同时存在时,取两者中较短的超时。当context取消时,底层TCP连接会被关闭(设置 req.Cancel 或关闭连接),不会等到服务器响应。
goroutine泄漏检测与防护
使用runtime监控goroutine数量,检测泄漏:
func monitorGoroutines(ctx context.Context) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
baseline := runtime.NumGoroutine()
for {
select {
case <-ticker.C:
current := runtime.NumGoroutine()
if current > baseline*2 {
log.Warn("goroutine count spike",
"baseline", baseline,
"current", current)
}
baseline = current
case <-ctx.Done():
return
}
}
}
防护模式总结:
1. 所有启动goroutine的地方必须传入context,goroutine内必须在阻塞操作前检查 ctx.Done()。
2. 使用带缓冲的channel(容量1)传递结果,避免父goroutine退出后子goroutine永久阻塞在channel写入。
3. 使用 errgroup 管理goroutine生命周期,它自动处理context传播和错误收集:
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return taskA(ctx)
})
g.Go(func() error {
return taskB(ctx)
})
if err := g.Wait(); err != nil {
// 任一goroutine返回错误,其余自动取消
}
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-context-chao-shi-chuan-bo-yu-goroutine-xie-lou/