如何使用Golang处理网络超时错误_Golang请求超时异常处理方法

Go的http.Client默认无超时,需显式设置Timeout或Transport各阶段超时;推荐用context.WithTimeout实现请求级可取消超时,并通过errors.Is判断标准错误;HTTP/2下需注意连接复用对超时的影响。

Go 的 http.Client 默认不带超时,必须手动设置

Go 标准库的 http.Client 实例默认没有超时控制,一旦后端卡住、网络丢包或 DNS 解析失败,Do() 会无限阻塞。这不是 bug,是设计选择 —— 超时策略必须由使用者根据业务场景明确声明。

正确做法是初始化 http.Client 时显式配置 Timeout,或更精细地用 Transport 控制各阶段耗时:

client := &http.Client{
    Timeout: 10 * time.Second,
}

注意:Timeout 是整个请求的总耗时上限(从 Do() 开始到响应体读完),它会覆盖 Transport 中的 DialContextTLSHandshakeResponseHeader 等子超时。如果需要分阶段控制(比如只限制连接建立时间),就得单独配 Transport

context.WithTimeout 实现请求级可取消超时

当请求已发出但想在中途主动中断(例如用户关闭页面、上游服务降级),仅靠 Client.Timeout 不够 —— 它无法响应外部信号。这时必须把 context.Context 传入 http.Request

  • context.WithTimeout 创建带截止时间的上下文
  • http.NewRequestWithContext 构造请求,而非 http.NewRequest
  • 超时触发时,Do() 会立即返回 context.DeadlineExceeded 错误

示例:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, "GET", "https://www./link/46b315dd44d174daf5617e22b3ac94ca", nil) resp, err := client.Do(req) if err != nil { if errors.Is(err, context.DeadlineExceeded) { // 处理超时 log.Println("request timeout") } return }

⚠️ 常见错误:忘记调用 cancel(),会导致 goroutine 泄漏;或在 Do() 后才创建 context,失去控制力。

区分 net/http 中三类典型超时错误

Go 的 HTTP 错误类型混杂,单看 err.Error() 容易误判。真正可靠的判断方式是用 errors.Is() 或类型断言:

  • context.DeadlineExceeded:context 超时(最常见,对应 WithTimeoutWithDeadline
  • net/http.httpError 包裹的底层错误:如 net.OpError(连接拒绝、无路由)、net.DNSError(DNS 解析失败)
  • io.EOFio.ReadFull 错误:服务端提前关闭连接、响应体不完整

建议统一处理逻辑:

if err != nil {
    switch {
    case errors.Is(err, context.DeadlineExceeded):
        // 业务超时
    case errors.Is(err, context.Canceled):
        // 主动取消
    case strings.Contains(err.Error(), "timeout"):
        // 底层 net.Dial timeout(如 Transport.DialContext 超时)
    default:
        // 其他网络错误
    }
}

不要依赖字符串匹配 "timeout",它不稳定;优先用 errors.Is() 判断标准错误变量。

HTTP/2 和连接复用对超时的影响

启用 HTTP/2(Go 1.6+ 默认开启)后,长连接复用会改变超时表现:

  • 单个 TCP 连接上多个请求共享 Transport.IdleConnTimeout(默认 30s),空闲连接会被回收
  • 若某次请求因超时被中断,该连接可能被标记为“损坏”,后续请求会新建连接,带来额外延迟
  • Transport.ResponseHeaderTimeout 只控制从发送请求头到收到响应头的时间,对 HTTP/2 的 header 帧生效,但不适用于流式响应

生产环境建议显式配置 Transport,避免默认值引发意外行为:

transport := &http.Transport{
    DialContext: (&net.Dialer{
        Timeout:   5 * time.Second,
        KeepAlive: 30 * time.Second,
    }).DialContext,
    TLSHandshakeTimeout: 5 * time.Second,
    ResponseHeaderTimeout: 3 * time.Second,
    IdleConnTimeout:       90 * time.Second,
    MaxIdleConns:          100,
    MaxIdleConnsPerHost:   100,
}
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}

超时不是越短越好。过短的 DialContext 会频繁重试连接,反而加重服务端压力;过长的 IdleConnTimeout 可能导致连接堆积。得结合监控数据反复调整。