3.Go 的 Context:超时了为什么还在跑?
wabicai
# 3.Go 的 Context:超时了为什么还在跑?
先记结论:Context 传的是取消信号和截止时间,不会强制杀掉 goroutine。一个函数如果完全不看 ctx,外面调用了 cancel(),它也可能继续跑。
# 请求到下游,Context 怎么传?
HTTP 处理函数可以从 r.Context() 拿到请求的上下文。调用数据库或外部接口时继续往下传,不要在中途换成 context.Background()。如果某个下游最多只等两秒,可以从上游 ctx 派生一个更短的超时:
func fetchStatus(ctx context.Context, client *http.Client, url string) (int, error) {
callCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(callCtx, http.MethodGet, url, nil)
if err != nil {
return 0, err
}
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
return resp.StatusCode, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这里省略了 import 和响应体处理。defer cancel() 即使调用成功也要写,及时释放派生上下文占用的资源。客户端断开或上游取消时,派生的 callCtx 也会收到取消信号。
# 取消和超时不是一回事
context.Canceled:有人主动取消了,例如请求被中断。context.DeadlineExceeded:超过了设置的截止时间。- 其他错误:可能是 DNS、连接、下游状态码或业务本身的问题,不要一概记为“超时”。
判断错误时可以用 errors.Is(err, context.Canceled) 和 errors.Is(err, context.DeadlineExceeded)。但“设置了两秒”不等于所有下游都必然在两秒内停下;调用的 API 要支持 Context,自己的循环也要检查 ctx.Done()。
# 排查时看这三处
- 上游的
ctx有没有一路传到数据库、HTTP 客户端或工作循环。 WithTimeout返回的cancel有没有调用。- 重试有没有沿用剩余时间,而不是每次重新给自己完整的两秒。
资料:Go Context 文档 (opens new window)、取消进行中的操作 (opens new window)。