必须显式配置http.Client,因http.Get/Post使用无超时的DefaultClient,易致DNS/TCP/TLS卡死、连接不复用、重定向失控、Body未关致fd耗尽;须定制Transport并配合context控制超时。

别直接用 http.Get 和 http.Post,它们背后共用 http.DefaultClient,而这个 client 的 Timeout 是零值,不设超时、不控重定向、不复用连接、Header 难设置、Body 必须手动关——线上一压就卡死或泄漏。
为什么 http.Get 会永久阻塞
它调的是 http.DefaultClient.Get,而 DefaultClient.Timeout 默认为 0,意味着无限等待 DNS 解析、TCP 握手、TLS 协商或首字节响应。哪怕目标服务已下线,goroutine 也会卡在 net.Conn.Read 或 net.Dial 阶段,无法被 cancel。
- 常见现象:日志停在
http.Get("https://slow-or-dead.example.com")后再无输出,ps -T能看到大量 sleeping goroutine - 根本原因:没有绑定
context.Context,底层 TCP 连接建立失败时无法中断 - 错误处理无效:即使加了
if err != nil,err 可能几秒甚至几分钟后才返回,且类型难区分(是超时?DNS 错?还是 TLS 版本不兼容?)
怎么发一个带超时的 GET 请求
核心是三步:构造带 context 的 request → 创建带 timeout 的 client → 显式调 Do。
- 用
context.WithTimeout包裹上下文,而不是time.After+select—— 后者不能中断正在 dial 的连接 -
http.NewRequestWithContext第三个参数传nil(GET 不需要 body),但必须检查返回的err(比如 URL 格式错) -
http.Client的Timeout字段只控制整个请求生命周期(从 dial 到读完 body),若需更细粒度(如仅限制 dial 时间),得配自定义Transport - 示例:
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil) if err != nil { // 处理 URL 解析失败等早期错误 } client := &http.Client{} resp, err := client.Do(req) if err != nil { // 此处 err 可能是 context.DeadlineExceeded、net.OpError 等,可类型断言区分 } defer resp.Body.Close() // 必须放在这,哪怕 resp.StatusCode != 200
POST JSON 为什么总返回 415
http.Post 会把第二个参数当 Content-Type header 写入,但它仍用 DefaultClient,且不会校验你传的 body 类型是否匹配该 header。更关键的是:服务端收到请求后,发现 Content-Type: application/json 但 body 是 raw bytes(没经过 json.Marshal),或 body 是 strings.NewReader 但 header 没设对,就直接 415。
立即学习“go语言免费学习笔记(深入)”;
- 典型错误:
http.Post(url, "application/json", strings.NewReader(`{"x":1}`))—— header 设了,但 client 没 timeout,body 类型对,可依然卡死 - 正确做法:用
http.NewRequest构造,显式req.Header.Set("Content-Type", "application/json"),body 用bytes.NewBuffer(jsonBytes)或strings.NewReader(string(jsonBytes)) - 注意
json.Marshal可能返回 error(字段没json:tag、含func或chan),不检查就 panic - 别漏
resp.Body.Close():漏掉会导致连接池耗尽,后续所有请求 hang 在transport.roundTrip
自定义 http.Client 的 Transport 怎么配
默认 http.Transport 对多数场景够用,乱调反而出问题。重点只调两个值:
-
MaxIdleConns和MaxIdleConnsPerHost:默认都是 100,除非你明确看到netstat -an | grep :443 | grep TIME_WAIT | wc -l超过 65535,否则别动 -
IdleConnTimeout:建议设 30–90 秒,避免空闲连接长期占着 fd - 别设
DisableKeepAlives: true:除非你确定每个请求都独立且不关心性能,否则等于主动放弃连接复用 - 如果要代理或自定义 TLS,才需要替换
Transport.Proxy或Transport.TLSClientConfig,普通业务不用碰
真正容易被忽略的不是“怎么写”,而是“谁来关 Body”和“谁来管超时”。resp.Body.Close() 必须在 client.Do() 后立刻 defer,哪怕你只读 status code;超时必须由 client 或 context 控制,不能靠外层 timer kill goroutine —— Go 的 HTTP 客户端是“配置驱动”的,不是“调用驱动”的。


















