直接用 http.Client 写业务请求易出问题,因默认无超时、无重试、无统一 User-Agent、不自动处理重定向或错误响应体,易致 goroutine 泄漏或请求卡死;重复写 ctx.WithTimeout、Header 设置、io.ReadAll 和错误判断导致代码散乱难测。

为什么直接用 http.Client 写业务请求容易出问题
因为默认的 http.Client 没有超时控制、没有重试、不带统一 User-Agent、不自动处理重定向或错误响应体,线上一跑就出现 goroutine 泄漏或请求卡死。更麻烦的是,每个接口都重复写 ctx.WithTimeout、req.Header.Set、io.ReadAll 和错误判断,代码散乱还难测。
封装核心:用结构体组合 http.Client 而不是继承
Go 不支持类继承,硬套“基类”思维反而导致接口膨胀。正确做法是定义一个结构体,内嵌 *http.Client 并附加配置字段和方法:
type HTTPClient struct {
client *http.Client
timeout time.Duration
userAgent string
retry int
}这样既能复用 http.Client 的所有能力(比如 Transport、CheckRedirect),又便于注入 mock client 做单元测试。别把超时逻辑塞进方法参数里——Do(ctx, req) 看似灵活,但业务层每次都要构造 context,实际反而增加出错概率。
Get 和 Post 方法该接受什么参数
业务最常遇到的是 URL 拼接、Query 参数、JSON Body、Header 注入这四件事。不要设计成「万能参数 map」,而是分层暴露:
立即学习“go语言免费学习笔记(深入)”;
-
Get(url string, query url.Values)—— query 用url.Values,避免手拼字符串导致编码错误 -
PostJSON(url string, body interface{})—— body 直接接受 struct/map,内部用json.Marshal,不暴露*bytes.Reader - 所有方法第一参数是
context.Context,但默认提供Do方法供高级用户接管底层http.Request - 返回值统一为
([]byte, error),不拆 status/code/body —— 大部分业务只关心“成功拿到数据”或“失败”,状态码解析留给调用方按需处理
重试和超时必须分开控制
很多人把重试和超时混在一起设:比如 “总超时 10s,重试 3 次”,结果第一次请求卡住 9s,剩下两次根本没机会发。正确做法是:
- 每次请求单独设超时:
ctx, cancel := context.WithTimeout(ctx, c.timeout) - 重试只针对特定错误:
url.Error、net.OpError、HTTP 5xx,但跳过 4xx(比如 404/401 是业务错误,重试无意义) - 重试间隔用指数退避:
time.Second * (2 ,避免雪崩 - 别在重试里重用同一个
http.Request.Body—— 它是一次性的,必须重新json.Marshal或从 bytes.NewReader 构造
真正难的不是写重试逻辑,而是判断哪些错误值得重试。网络抖动、连接拒绝、TLS 握手失败可以重试;JSON 解析失败、400 Bad Request、空响应体这些,重试只会放大问题。


















