应类型断言到net.Error,通过Timeout()和Temporary()区分超时、连接拒绝等错误;用context.WithTimeout精细控制超时,避免Client.Timeout冲突;重试前必检查ctx.Done()防止goroutine泄漏。

判断 NetError 并区分超时与连接拒绝
Go 的网络错误不是统一类型,直接用 err == nil 或 strings.Contains(err.Error(), "timeout") 都不可靠。真正该做的是类型断言到 net.Error,再看 Timeout() 和 Temporary() 方法返回值。
常见错误现象:把 connection refused 当成可重试的临时错误,结果反复连失败的服务;或者把 DNS 解析超时当成永久错误,直接放弃重试。
-
net.Error.Timeout()为true→ 明确是超时(如context.DeadlineExceeded、底层读写超时) -
net.Error.Temporary()为true但Timeout()为false→ 可能是连接被拒、地址不可达等,多数情况也适合重试 - 两者都为
false→ 比如证书错误、协议不匹配,这类不该重试
示例判断逻辑:
if nerr, ok := err.(net.Error); ok {
if nerr.Timeout() {
// 超时,可重试
} else if nerr.Temporary() {
// 临时性网络问题,如 connection refused、no route to host
}
}用 context.WithTimeout 控制单次请求超时,而非依赖 http.Client.Timeout
http.Client.Timeout 看似方便,但它会覆盖整个请求生命周期(DNS + 连接 + TLS + 发送 + 接收),且无法在中途取消。实际中你往往需要更精细的控制:比如连接阶段最多等 2 秒,而响应体下载允许 30 秒。
立即学习“go语言免费学习笔记(深入)”;
使用场景:调用下游 HTTP API 时,既要防住慢 DNS 或卡死的 TCP 握手,又不能因大文件响应导致整体阻塞。
- 永远优先用
context.WithTimeout包裹http.Do,而不是只设Client.Timeout -
Client.Timeout建议设为 0(禁用),避免和 context 冲突 - 如果需分段超时(如 connect ≤ 1s,read ≤ 5s),得换用
http.Transport的DialContext和ResponseHeaderTimeout等字段
重连逻辑里别忽略 context.Canceled 和 context.DeadlineExceeded
重试不是无条件循环。很多人写了 for + sleep,但没检查上层 context 是否已取消,导致 goroutine 泄漏或超时后还在傻等。
常见错误现象:HTTP handler 已返回 504,但后台重试 goroutine 还在跑,甚至发起第 5 次请求。
- 每次重试前必须用
select检查ctx.Done(),并返回ctx.Err() - 不要在重试循环里用
time.Sleep后再检查 context —— 睡眠期间 context 可能已取消 - 推荐用
time.AfterFunc或timer.Reset配合 select,避免 sleep 阻塞
简短示意:
for i := 0; i < maxRetries; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// 执行请求...
if isRetryable(err) {
time.Sleep(backoff(i))
continue
}
return err
}重试间隔用指数退避,但注意别让第一次重试太“激进”
固定间隔(如每次都 sleep 100ms)在真实网络抖动下效果差:要么重试太猛压垮下游,要么太慢拖长用户等待。指数退避是标准解法,但容易忽略两个细节:初始间隔不能为 0,以及要加随机抖动(jitter)。
性能影响:没有 jitter 的指数退避,在服务集体重启时会引发“重试风暴”,所有客户端在同一时刻重连。
- 初始间隔建议 ≥ 100ms,比如
base = 100 * time.Millisecond - 每次重试:
time.Duration(float64(base) * math.Pow(2, float64(attempt))) - 务必乘上
0.5 ~ 1.5的随机因子,用rand.Float64()实现 - 最大间隔建议 capped,比如不超过 5 秒,避免单次重试等待过久
复杂点其实在 jitter 的实现方式 —— 如果用全局 *rand.Rand,要注意并发安全;用 math/rand 的 local seed 更稳妥,但别在循环里反复 rand.Seed(time.Now().UnixNano())。










