判断go网络超时错误应先类型断言为net.error再调用timeout()方法,而非依赖errors.is(err, context.deadlineexceeded)或err==net.errtimeout,因前者仅适用于context超时,后者极少被返回;且需结合op字段区分dial/write/read等场景以决定是否重试。

怎么判断一个 Go 错误是不是网络超时错误
直接用 errors.Is(err, context.DeadlineExceeded) 或 net.ErrTimeout 不可靠——前者只适用于 context.WithTimeout 触发的错误,后者极少被底层直接返回。真正稳定的方式是类型断言到 net.Error 接口并调用其 Timeout() 方法。
常见错误现象:用 strings.Contains(err.Error(), "timeout") 判断,结果在不同系统(比如 macOS vs Linux)或不同 Go 版本下行为不一致;或者只检查 err == net.ErrTimeout,但 HTTP 客户端、DNS 解析、TLS 握手等场景根本不会返回这个变量。
- 必须先做类型断言:
if nerr, ok := err.(net.Error); ok && nerr.Timeout() -
net.Error是接口,标准库中net.OpError、http.httpError(私有)、tls.alertError等都实现了它 - 注意:有些错误(如连接被拒绝)也实现了
net.Error,但Timeout()返回false,所以不能省略该方法调用
HTTP 客户端超时判断为什么经常漏掉 TLS 握手超时
HTTP 默认使用 DefaultTransport,它的 DialContext 和 TLSClientConfig.HandshakeTimeout 是两套独立超时机制。如果只设了 http.Client.Timeout,它只覆盖整个请求生命周期(含 DNS、连接、TLS、发送、响应),但无法精确控制 TLS 握手阶段是否单独超时;而 Timeout() 在 TLS 握手失败时仍可能返回 true,前提是底层错误确实实现了 net.Error 并标记为超时。
-
http.Client.Timeout被触发时,错误通常是context.DeadlineExceeded,不满足net.Error断言,此时Timeout()无从调用 - 要捕获 TLS 握手超时,得自定义
http.Transport,设置TLSClientConfig.HandshakeTimeout,这样失败时底层会返回带Timeout() == true的net.OpError - Go 1.19+ 中,
http.DefaultTransport的 TLS 握手默认无超时,容易卡住,必须显式配置
Timeout() 返回 true 却不是你预期的“业务超时”
net.Error.Timeout() 只表示“底层 I/O 操作因时间限制被中断”,和你的业务逻辑是否“应该重试”没有必然关系。比如 TCP 连接阶段超时(connect timeout)通常应重试,而读取响应体中途超时(read timeout)可能已部分接收数据,重试就可能重复提交。
立即学习“go语言免费学习笔记(深入)”;
- 连接建立超时:
err.(*net.OpError).Op == "dial" - 写入超时:
err.(*net.OpError).Op == "write" - 读取超时:
err.(*net.OpError).Op == "read" - 不要仅凭
Timeout()就统一重试——先看Op字段,再结合协议语义决定行为
自定义 net.Error 实现时 Timeout() 容易写错
如果你在封装底层错误(比如加 trace ID、包装 error chain),又想保留 Timeout() 行为,别直接返回 false 或硬编码 true。正确做法是递归检查原始错误是否满足 net.Error 且 Timeout() 为真。
- 错误写法:
func (e *MyError) Timeout() bool { return false }—— 丢弃了原始超时信息 - 推荐写法:
if nerr, ok := e.err.(net.Error); ok { return nerr.Timeout() },再 fallback 到其他判断逻辑 - 注意 error wrap 链:Go 1.13+ 的
errors.Unwrap不保证返回net.Error,仍需逐层断言
真正麻烦的是那些既没实现 net.Error、又没透出底层错误的中间件或 SDK —— 它们把超时吞成 generic error,这时候 Timeout() 根本没机会调用。遇到这种库,只能翻源码确认错误构造逻辑,或者换用更透明的替代方案。










