httputil.NewSingleHostReverseProxy 是首选,因其为Go官方维护、自动处理hop-by-hop头过滤、重定向重写等关键逻辑;需通过Director重写URL和Host,显式设置X-Forwarded-For,CONNECT方法须hijack处理,Body读取后须重建并设ContentLength,Transport需独立配置超时与连接池。

别用框架封装的代理逻辑,直接上标准库 httputil.NewSingleHostReverseProxy —— 它比大多数“轻量框架”更稳、更少隐藏坑。
为什么 httputil.NewSingleHostReverseProxy 是首选
它不是“黑盒”,而是 Go 官方维护的反向代理核心实现,自动处理:hop-by-hop 头过滤(Connection、Keep-Alive 等)、Content-Length 与 Transfer-Encoding 冲突修复、重定向 Location 重写、连接复用管理。手动拼 http.NewRequest + client.Do 容易漏掉其中任意一项,导致 400/502/空响应。
常见错误现象包括:后端收不到 Authorization 头、POST 请求体为空、HTTPS 后端返回 403(因 Host 不匹配)、长连接下内存缓慢增长。
- 必须通过
Director函数重写req.URL和req.Host,不能只改 URL 字符串 -
req.URL.RawPath要同步更新,否则含中文或特殊字符的路径会触发 400 - 若需添加
X-Forwarded-For或自定义头,必须在Director里显式设置,它不会自动透传
CONNECT 方法必须 hijack,不能走 HTTP 流程
浏览器访问 HTTPS 站点前发的是 CONNECT google.com:443,这不是 HTTP 请求,而是一个 TCP 隧道协商指令。任何试图用 http.ServeHTTP 或 ReverseProxy.ServeHTTP 处理它的做法都会失败 —— 你会看到浏览器卡死、超时,或后端返回奇怪的 405 错误。
立即学习“go语言免费学习笔记(深入)”;
正确做法是在主 handler 中单独判断:
- 检测
req.Method == "CONNECT",立即返回"HTTP/1.1 200 Connection Established\r\n\r\n" - 调用
w.(http.Hijacker).Hijack()获取原始net.Conn,注意类型断言失败要兜底返回错误 - 用
net.DialTimeout("tcp", req.Host, 10*time.Second)连后端,不是http.Get - 启动两个
io.Copy协程双向转发字节流,并关闭所有连接的读写超时(conn.SetDeadline(time.Time{}))
Body 读取一次就没了,转发前必须重建
如果你在中间加了日志、鉴权或限流逻辑,并调用了 io.ReadAll(req.Body),那么后续转发时 req.Body 就是空的 —— http.Transport 不会报错,但后端收不到 POST 数据,JSON 解析失败,状态码却是 200。
解决方法不是“避免读 Body”,而是读完立刻重建:
- 用
bodyBytes, _ := io.ReadAll(req.Body)拿到原始字节 - 设
req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - 显式设置
req.ContentLength = int64(len(bodyBytes)),否则 Go 会设为 -1,某些后端(如 Nginx)直接拒收 - 大 Body(>2MB)慎用全内存缓存;可考虑
io.TeeReader+ 临时文件,但得自己清理
Transport 超时不配等于裸奔
直接用 http.DefaultTransport 转发,压测时大概率遇到 net/http: request canceled (Client.Timeout exceeded while awaiting headers) 或 read tcp i/o timeout。错误堆栈里没有你自己的代码,因为超时发生在 Transport 底层。
每个后端应配独立 http.Transport 实例,关键参数必须显式设:
-
DialContext里设Timeout(建议 ≤ 3s)和KeepAlive -
TLSHandshakeTimeout(≤ 3s),防止 TLS 握手卡住 -
ResponseHeaderTimeout(≤ 5s),控制从发完请求到收到响应头的时间 -
MaxIdleConns和MaxIdleConnsPerHost根据并发量调,别用默认 0(不限制)
最常被忽略的一点:不要复用同一个 http.Client 实例去转发不同后端的请求 —— Transport 的连接池策略、TLS 配置、超时参数都可能冲突,导致某一个后端的抖动拖垮全部流量。


















