直接用httputil.NewSingleHostReverseProxy可快速启动代理,但生产环境必须通过Director显式设置req.URL.Scheme、req.URL.Host、req.Host,并手动透传X-Forwarded-For、Upgrade等关键头,否则易出现502/400错误或WebSocket握手失败。

直接用 httputil.NewSingleHostReverseProxy 就能跑通,但别急着上线
Go 标准库的 httputil.NewSingleHostReverseProxy 确实三行代码就能转发请求,比如代理到 http://127.0.0.1:8080。但它默认不处理 Host 头、不透传原始客户端 IP、不校验 TLS 证书(如果后端是 HTTPS),更不支持多目标路由。直接拿来当生产网关用,大概率会在真实请求里翻车。
常见错误现象包括:502 Bad Gateway(后端拒绝连接)、400 Bad Request(Host 头缺失或错乱)、WebSocket 连接被静默关闭(Upgrade 头未透传)。
-
Director函数必须显式设置,否则Host和URL.Path不会重写,后端服务收不到正确路径 - 若后端是 HTTPS,需自定义
Transport并配置TLSClientConfig.InsecureSkipVerify(仅测试用)或提供 CA 证书 - WebSocket 支持需要手动复制
Connection、Upgrade、Sec-WebSocket-Key等头,否则握手失败
Director 函数怎么写才不丢请求头和路径
这是反向代理最常被忽略的一环。Director 不只是改 req.URL.Host,它决定了请求是否能被后端正确识别。标准写法里,req.URL.Scheme、req.URL.Opaque、req.Host 都得同步更新,否则后端可能返回 400 或 301 跳转。
典型安全写法:
立即学习“go语言免费学习笔记(深入)”;
proxy.Director = func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "10.0.0.1:8080"
req.Host = "10.0.0.1:8080" // 强制覆盖 Host 头,避免透传原始域名
// 保留原始路径,不额外拼接 "/"
if req.URL.Path == "" {
req.URL.Path = "/"
}
}
- 不要用
req.URL.Path = "/" + req.URL.Path,会导致双斜杠//api/users -
req.Host必须设,否则某些后端(如 Nginx 默认配置)会因 Host 不匹配返回 444 - 如需透传客户端真实 IP,得手动加
X-Forwarded-For:req.Header.Set("X-Forwarded-For", req.RemoteAddr)
HTTPS 和 WSS 代理必须手动透传 Upgrade 头
标准 ReverseProxy 对 WebSocket 和 TLS 升级请求是“盲处理”的——它不会自动识别 Upgrade: websocket 请求,也不会把 Connection: upgrade 带过去。结果就是客户端连上,握手阶段卡死,浏览器控制台报 WebSocket connection to 'wss://...' failed。
解决方法不是换库,而是补全关键头:
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
proxy.ServeHTTP = func(rw http.ResponseWriter, req *http.Request) {
// 显式允许升级
if req.Header.Get("Upgrade") == "websocket" {
rw.Header().Set("Connection", "upgrade")
rw.Header().Set("Upgrade", "websocket")
}
proxy.ServeHTTP(rw, req)
}
-
Upgrade和Connection头必须成对出现在响应中,缺一不可 - HTTPS 后端若用自签名证书,
InsecureSkipVerify: true是临时方案;生产环境务必加载可信 CA 证书到TLSClientConfig.RootCAs - WSS 代理还依赖底层 TCP 连接保持,Go 的
http.Transport默认启用 keep-alive,一般无需额外设置
多个后端怎么路由?别硬编码 if-else
单目标代理容易,但真实场景要按路径前缀(如 /api/user → user-svc)、子域名(admin.example.com → admin-svc)分发。硬写一堆 if req.URL.Path.startswith(...) 会导致逻辑耦合、无法热更新、难测难维护。
推荐用轻量路由表 + 动态代理实例:
type Route struct {
Match string // e.g. "/api/order"
Proxy *httputil.ReverseProxy
}
routes := []Route{
{"/api/user", httputil.NewSingleHostReverseProxy(userURL)},
{"/api/order", httputil.NewSingleHostReverseProxy(orderURL)},
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
for _, route := range routes {
if strings.HasPrefix(r.URL.Path, route.Match) {
route.Proxy.ServeHTTP(w, r)
return
}
}
http.Error(w, "Not Found", http.StatusNotFound)
})
- 路径匹配要用
strings.HasPrefix,别用==,否则/api/user/123匹配不上/api/user - 每个
ReverseProxy实例应独享Transport,避免连接池混用导致超时或复用错误连接 - 路由规则变更时,需重建
routes切片并替换处理器(可用原子指针或 sync.RWMutex 控制并发读写)
真正麻烦的从来不是写几行代理代码,而是让每条请求都带着正确的上下文穿过代理层——Host、TLS、Upgrade、Forwarded-For,漏掉任何一个,下游服务就可能拒收或行为异常。


















