应自己封装 http.Client,因其可配置超时、连接复用、重试、日志及中间件,避免默认客户端无超时、连接耗尽、请求卡死等问题。

为什么要自己封装 http.Client 而不是直接用默认客户端
Go 的 http.DefaultClient 看似方便,但实际项目中几乎不能直接用:它没有超时控制(底层 http.Transport 的 DialContext 和 ResponseHeaderTimeout 全是 0),复用连接能力弱,默认不带重试和日志,更无法注入中间件逻辑。微服务调用、第三方 API 集成、压测场景下,裸用会导致连接耗尽、请求卡死、错误难追踪。
- 默认客户端的
Timeout字段只作用于整个请求生命周期(Go 1.19+ 才支持),但底层 TCP 连接、TLS 握手、首字节等待仍可能无限挂起 - 所有请求共享同一套
http.Transport,一旦某个域名 DNS 解析失败或 TLS 协商卡住,可能阻塞后续所有请求 - 无法统一添加
User-Agent、Authorization、请求 ID、链路追踪 header
封装核心:自定义 http.Transport + 可配置的 http.Client
关键不是“写个新 client”,而是控制底层传输行为。必须显式构造 http.Transport 并设置以下参数:
-
MaxIdleConns和MaxIdleConnsPerHost设为非 0 值(如 100),否则 HTTP/1.1 连接不会复用 -
IdleConnTimeout建议设为 30s,避免长连接被服务端主动断开后 client 还在傻等 -
TLSHandshakeTimeout必须设(如 10 * time.Second),否则 TLS 握手失败会卡住整个 goroutine - 使用
http.ProxyFromEnvironment或自定义代理函数,别忽略公司内网环境
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
Proxy: http.ProxyFromEnvironment,
// 若需跳过证书校验(仅测试),加这一行:TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}如何支持请求重试和上下文传递
Go 的 http.Client 本身不提供重试机制,也不能在请求中途安全中断正在写的 body。正确做法是在调用层封装一个带重试逻辑的函数,且每次重试都新建 *http.Request —— 因为 req.Body 是单次读取的,重用会 panic。
- 用
context.WithTimeout或context.WithDeadline控制单次请求,不是给整个 client 设 timeout - 重试间隔建议用指数退避(如 100ms → 200ms → 400ms),避免雪崩
- 只对幂等方法(
GET、HEAD、OPTIONS)或明确可重试的状态码(502/503/504)重试,POST默认不重试
func (c *HTTPClient) DoWithRetry(ctx context.Context, req *http.Request, maxRetries int) (*http.Response, error) {
var err error
for i := 0; i <= maxRetries; i++ {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
resp, err := c.client.Do(req.WithContext(ctx))
if err == nil && isRetriableStatusCode(resp.StatusCode) {
_ = resp.Body.Close()
if i == maxRetries {
break
}
time.Sleep(time.Duration(math.Pow(2, float64(i))) * 100 * time.Millisecond)
// 注意:这里必须重建 req,不能复用!
req, _ = http.NewRequestWithContext(ctx, req.Method, req.URL.String(), req.Body)
continue
}
if err == nil {
return resp, nil
}
}
return nil, err
}容易被忽略的坑:Body 没关闭、JSON 错误处理、URL 编码
封装时最常漏掉三件事:一是 resp.Body 必须手动 Close(),否则连接永远不释放;二是 json.Unmarshal 前没检查 resp.StatusCode,导致 4xx/5xx 响应体也被当正常 JSON 解析;三是拼 URL 时没做 url.PathEscape,含中文或特殊字符的 path 直接 400。
立即学习“go语言免费学习笔记(深入)”;
- 所有
Do后的resp.Body必须用defer resp.Body.Close(),哪怕后面要ioutil.ReadAll - 不要写
json.NewDecoder(resp.Body).Decode(&v)就完事,先判断resp.StatusCode - 构建 URL 时,path 段用
url.PathEscape,query 参数用url.QueryEscape,别手拼字符串
封装不是为了造轮子,是把 Go HTTP 底层那些「默认不安全」的选项,变成项目里可审计、可配置、可观测的确定行为。越早统一 client 行为,后期排查超时、连接数暴涨、证书错误这类问题就越省力。










