roundtripper 是 http 客户端执行网络调用的核心接口,需通过替换 transport 字段完全替代默认实现并委托给 http.defaulttransport 以保留连接复用等能力;常见用法是实现日志记录、header 修改等逻辑,注意 body 读取后需重建、context 取消必须透传。

RoundTripper 是什么,为什么不能直接替换 Transport.RoundTrip
Go 的 http.Client 发起请求时,真正执行网络调用的是其 Transport 字段持有的 RoundTripper 实例。默认是 http.DefaultTransport,它实现了 RoundTripper 接口——核心就是 RoundTrip(*http.Request) (*http.Response, error) 方法。
你不能“拦截”默认实现,因为它是封装好的结构体;但你可以**提供自己的实现**,只要满足接口签名。关键点:不是“修改”原 RoundTripper,而是**完全替代它**,并把原始逻辑委托给底层(比如 http.DefaultTransport)来保留 DNS、TLS、连接复用等能力。
写一个基础 RoundTripper 包裹器:日志 + 委托
最常见需求是打日志或改 Header。你需要在自己的 RoundTrip 方法里做前置/后置处理,再调用底层 transport 的 RoundTrip。注意:必须复用同一个底层 transport 实例(不能每次 new),否则会丢失连接池和 keep-alive。
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())
resp, err := l.rt.RoundTrip(req)
if err != nil {
log.Printf("← error: %v", err)
} else {
log.Printf("← %d %s", resp.StatusCode, resp.Status)
}
return resp, err
}
// 使用方式:
client := &http.Client{
Transport: &loggingRoundTripper{rt: http.DefaultTransport},
}
- 别漏掉
rt字段初始化,否则 panic - 不要在
RoundTrip里 newhttp.Transport—— 这会丢掉连接复用 - 如果需要并发安全,确保底层
rt本身是线程安全的(http.DefaultTransport是)
修改请求或响应 Body:必须考虑 io.ReadCloser 生命周期
Body 是 io.ReadCloser,只能读一次。想读取并重放,必须用 io.NopCloser + bytes.NewReader 重建,且要小心内存占用(大 Body 需流式处理)。
func (m *bodyModifier) RoundTrip(req *http.Request) (*http.Response, error) {
// 读取原始 body
bodyBytes, _ := io.ReadAll(req.Body)
req.Body.Close()
// 修改 body 内容(例如注入 trace ID)
modified := bytes.ReplaceAll(bodyBytes, []byte("X-Trace-ID"), []byte("X-Trace-ID: abc123"))
// 重新赋值 body
req.Body = io.NopCloser(bytes.NewReader(modified))
return m.rt.RoundTrip(req)
}
- 必须调用
req.Body.Close(),否则连接可能无法复用 -
io.ReadAll会把整个 body 加载进内存 —— 大文件上传场景慎用 - 若需流式修改(如 gzip 压缩),得用
io.TeeReader或自定义ReadCloser
超时与 Cancel Context 的传递不能被绕过
Go 的 http.Client 依赖 context 控制请求生命周期。如果你在 RoundTrip 中启动 goroutine 或做异步操作,必须显式检查 req.Context().Done(),否则会泄漏 goroutine 或忽略 cancel。
更稳妥的做法是:**所有逻辑必须同步执行,并让底层 transport 处理 context 取消**。你自己只做轻量级预处理(Header、URL、Body 替换等),不阻塞、不新建 goroutine。
- 不要在
RoundTrip里用time.Sleep或select { case - 不要自己调用
req.Context().Cancel()—— 这由 client 和 transport 管理 - 如果要做重试,应该用
http.Client.CheckRedirect或外部 wrapper,而非在 RoundTripper 里轮询
真正难的不是写个 RoundTripper,而是让它既不破坏原有语义,又不引入竞态或资源泄漏。Body 重放、context 传播、连接池复用,这三处最容易出问题。











