net/http.roundtrip是http.roundtripper接口的唯一方法,用于执行单个http事务;http.transport是该接口最核心、默认的实现,所有http.client请求最终都经由transport.roundtrip发出。

什么是 net/http.RoundTrip,它和 Transport 什么关系
RoundTrip 是 http.RoundTripper 接口的唯一方法,http.Transport 是它的最常用实现。你不能直接调用 RoundTrip 来发请求(那是 http.Client.Do 干的事),但可以通过自定义 RoundTripper 在请求真正发出前/响应返回后插入逻辑——这就是拦截器的本质。
关键点:所有请求最终都流经 Transport.RoundTrip;替换默认 Transport 或包装它,就能拦截。
如何写一个基础请求拦截器(带日志和 Header 修改)
最常见需求是加日志、统一加 Authorization、改 User-Agent。不要重写整个 Transport,而是用“包装器”模式:
type loggingRoundTripper struct {
rt http.RoundTripper
}
func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
log.Printf("→ %s %s", req.Method, req.URL.String())
// 修改请求(比如加 header)
req.Header.Set("X-Request-ID", uuid.New().String())
resp, err := l.rt.RoundTrip(req)
if err != nil {
log.Printf("✗ %v", err)
return nil, err
}
log.Printf("← %d %s", resp.StatusCode, req.URL.String())
return resp, nil
}
使用时:client.Transport = &loggingRoundTripper{rt: http.DefaultTransport}。注意:必须传入底层 RoundTripper(通常是 http.DefaultTransport 或自定义的),否则会 panic。
- 别在
RoundTrip里修改req.URL后不 deep-copy ——http.Transport内部可能复用*url.URL,导致并发下 URL 被意外覆盖 - 如果要修改
req.Body(比如加签名),必须确保它是可重读的(用io.NopCloser(bytes.NewReader(buf))替换),否则后续RoundTrip会读到空 body - 不要在拦截器里做耗时操作(如远程鉴权),会阻塞整个 client 的请求队列
如何拦截并修改响应 Body(比如解密或重写 JSON)
响应体拦截比请求体更危险:一旦 resp.Body 被读取一次,就无法再次读取。所以必须用 io.TeeReader 或完整缓存再重建 Response:
func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := l.rt.RoundTrip(req)
if err != nil {
return resp, err
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close() // 必须关,否则连接不释放
if err != nil {
return resp, err
}
// 修改 body,例如 JSON 字段重写
var data map[string]interface{}
if json.Unmarshal(body, &data) == nil {
data["intercepted"] = true
body, _ = json.Marshal(data)
}
resp.Body = io.NopCloser(bytes.NewReader(body))
resp.ContentLength = int64(len(body))
return resp, nil
}
注意:ContentLength 必须重设,否则 client 可能因长度不匹配而卡住;如果原响应是 chunked,ContentLength 为 -1,此时应删掉该字段而非硬设。
- 大响应体(>1MB)别全读进内存,考虑用
io.Pipe流式处理,但逻辑复杂得多 -
resp.Header可以安全修改,但resp.Trailer不行——它依赖底层连接状态 - 某些中间件(如 gzip 解压)可能已消费
resp.Body,需确认是否启用了Transport.ResponseHeaderTimeout或自定义DecompressResponse: false
为什么有时 RoundTrip 拦截器没生效
常见失效场景不是代码写错,而是 client 配置绕过了你的 Transport:
-
http.Client没设置Transport字段(用了默认值),结果你的拦截器根本没挂上去 - 用了
http.NewRequestWithContext但 context 带了httptrace或自定义net.Conn,某些 trace hook 会跳过 Transport - 第三方库(如
github.com/hashicorp/go-retryablehttp)自己封装了 Transport,需查文档看是否暴露HTTPClient设置入口 - 测试时用了
httptest.NewServer,它的 handler 不走 Transport,自然不会触发RoundTrip
验证方式:在 RoundTrip 开头加一行 log.Print("intercepted"),然后发个真实 HTTP 请求(别用 mock 或 test server),看日志是否输出。
真正难的不是写拦截器,而是理解哪些路径会绕过它、哪些字段改了会破坏连接复用或 HTTP/2 流控——这些细节藏在 net/http 源码的 transport.go 里,不读没法稳。











