用 Go 写反向代理时,最容易混淆的是“客户端发给代理的请求”和“代理准备发给后端的请求”。如果你要修改后端看到的 Host、路径或协议,应当操作 httputil.ProxyRequest.Out,而不是 ProxyRequest.In。在现代 Go 写法中,用 ReverseProxy.Rewrite 接收这个结构体,再按路由规则决定是否覆盖 Out.Host。
一句话结论:r.SetURL(target)负责改出站 URL,并会把出站 Host 交给目标主机;需要保留客户端原始 Host 时,在它后面写r.Out.Host = r.In.Host。不要把 Host 当成普通的Header项来改。
先分清 In、Out、URL 和 Host
ProxyRequest.In 是代理收到的请求,官方文档明确要求 Rewrite 不要修改它;ProxyRequest.Out 才是即将交给 Transport 的出站请求。URL 中的 Scheme、Host 和 Path 描述请求目标,而 http.Request.Host 是发送时使用的 Host 语义。它们不是同一个字段。
因此,下面这种思路是不对的:先把 r.In.Host 改成后端域名,再期待代理自动使用它。这样既破坏了入站语义,也没有明确修改将要发送的请求。
用 Rewrite 和 SetURL 建立最小代理
SetURL 适合把出站请求路由到固定目标。它会改写出站请求的协议、主机和基础路径,同时将 Out.Host 置空,让发送逻辑使用目标 URL 的主机。示例中的每个关键判断都放在 Rewrite 内,便于把“路由目标”和“Host 语义”分开。
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
target, err := url.Parse("http://backend.internal:8080/base")
if err != nil {
log.Fatal(err) // 目标地址解析失败时不要启动半配置的代理。
}
proxy := &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(target) // 修改 Out.URL,并默认让 Host 跟随目标主机。
r.SetXForwarded() // 由标准库写入来源相关的转发头。
},
}
log.Fatal(http.ListenAndServe(":8080", proxy)) // 代理作为 Handler 接收客户端请求。
}
这里不要把图示当成已经运行过的结果:它只是帮助区分字段的静态结构。真实项目还要按自身的域名信任、路径拼接和后端路由约定补充配置。
什么时候覆盖 Out.Host
如果后端虚拟主机依赖目标域名,保留 SetURL 的默认行为即可;如果后端必须看到客户端原始 Host,例如多租户路由由入口域名决定,则在 SetURL 后显式复制入站 Host:
proxy := &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(target) // 先确定实际连接的后端目标。
r.Out.Host = r.In.Host // 再保留客户端请求的 Host 语义。
r.SetXForwarded() // X-Forwarded-Host 记录入站 Host,供后端审计或路由。
},
}
这两个选择不是“哪个字段更正确”,而是后端如何识别站点:连接地址由 Out.URL 决定,Host 语义由 Out.Host 决定,来源记录则由 X-Forwarded-Host 等头表达。三者用途不同,不能用改 Header 的方式代替 Out.Host。
Rewrite、Director 和转发头的取舍
新代码优先使用 Rewrite,因为它直接提供 ProxyRequest.In 与 ProxyRequest.Out 两个角色。旧代码中的 Director 仍可工作,但它返回后还会进行 hop-by-hop 头处理,某些在 Director 中加入的头可能被移除;文档建议需要确保改动保留时使用 Rewrite。
SetXForwarded 会设置 X-Forwarded-For、X-Forwarded-Host 和 X-Forwarded-Proto。如果代理前面还有不可信代理,不要无条件信任客户端传入的转发头;应先按自己的信任边界清理或重建它们。最后,Rewrite 只负责改写请求,响应体关闭、超时和后端错误仍应由完整服务配置负责。
常见问题
Q:为什么改了 Header.Set("Host", ...) 后后端仍然看到旧值?
因为 Host 有独立的 Request.Host 语义,反向代理场景应修改 r.Out.Host;普通 Header 映射不是可靠替代。
Q:SetURL 后应该写目标 Host 还是 In.Host?
后端按目标域名做虚拟主机路由时保留默认值;后端按入口域名识别租户时写 r.Out.Host = r.In.Host,并用转发头记录来源。
Q:能不能直接修改 In?
不建议。In 是代理收到的原始请求,Rewrite 的职责是构造 Out,保留两者的边界更容易排查路由和审计问题。
官方资料:https://pkg.go.dev/net/http/httputil#ProxyRequest、https://pkg.go.dev/net/http/httputil#ReverseProxy、https://go.dev/src/net/http/httputil/reverseproxy.go

请求调试
