直接替换http.DefaultTransport会丢失连接池、TLS配置和超时策略,引发复用失效、超时异常及并发panic;正确做法是嵌入并包装http.Transport,在RoundTrip中调用原实现且克隆请求。

直接改 http.DefaultTransport 会崩,不是加个日志或重试就能完事——连接池丢、TLS 配置失、并发 panic 都是真实踩过的坑。必须用包装(wrapping)而非替换。
为什么不能直接赋值 http.DefaultTransport
很多人写 http.DefaultTransport = &myRT{} 后发现 http.Get 开始超时、复用失效、甚至 panic。根本原因是:http.DefaultTransport 是个已初始化的指针,裸赋值会丢掉它内部维护的连接池、TLSConfig、IdleConnTimeout 等状态;更危险的是,http.Transport 的字段(比如 MaxIdleConns)不是线程安全的,多 goroutine 并发写入会触发竞态。
正确做法:嵌入 + 包装 http.Transport
要加日志、重试、Header 修改等逻辑,必须保留原 http.Transport 的能力,只在 RoundTrip 入口/出口插入行为:
- 定义结构体,字段为
Transport http.RoundTripper(类型别名更安全),而非继承或覆盖 -
RoundTrip方法里调用t.Transport.RoundTrip(req),不是t.RoundTrip(req)(否则无限递归) - 修改
*http.Request前必须req.Clone(req.Context()),否则可能污染上游请求(尤其 Body 已被读过) - 重试时需重建
*http.Request,因为多数Body不可重放(bytes.Reader或支持Seek的除外)
示例片段:
立即学习“go语言免费学习笔记(深入)”;
type loggingTransport struct {
Transport http.RoundTripper
}
func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context()) // 安全克隆
start := time.Now()
resp, err := t.Transport.RoundTrip(req)
log.Printf("HTTP %s %s → %d (%v)", req.Method, req.URL, resp.StatusCode, time.Since(start))
return resp, err
}
常见拦截需求对应的关键点
不同场景下,RoundTrip 内部操作差异很大,稍不注意就出错:
-
过滤方法:检查
req.Method == http.MethodGet后直接return nil, errors.New(...),别调下游 -
改 Header:用
req.Header.Set()或Del(),但不要动Host字段(http.Transport会自动设置,手动设反而出错) -
改 Body:先
ioutil.ReadAll(req.Body),再req.Body.Close(),最后用bytes.NewReader()+ioutil.NopCloser()重建req.Body -
错误透传:网络层错误(如
net.OpError、url.Error)必须原样返回;业务级 4xx/5xx 是响应体内容,属于正常流程,不应当错误处理
真正难的不是写逻辑,而是判断哪些状态该保留、哪些字段该深拷贝、哪些错误该吞掉——http.Transport 的行为边界很细,一不留神就绕进连接复用或 TLS 握手的坑里。


















