Go语言无开箱即用API网关,须用net/http+httputil.NewSingleHostReverseProxy+chi等路由库组合实现;Director必改req.URL.Scheme、Host、Path,Transport需自定义连接池,ErrorHandler要返回JSON错误,X-Real-IP需过滤私有IP透传,路径重写须用TrimPrefix而非正则。

Go 语言没有开箱即用的 API 网关,但用 net/http + httputil.NewSingleHostReverseProxy + 路由库(如 chi)组合,能快速搭出可控、可调试、生产可用的轻量网关——关键不是“能不能跑”,而是“哪些默认值必须改”。
httputil.NewSingleHostReverseProxy 的三个必改字段
它只帮你做了最基础的 Host 替换,其余全是线上事故温床。不改这三项,压测时连接耗尽、超时卡死、IP 透传失效全是常态。
-
Director:必须重设req.URL.Scheme、req.URL.Host和req.URL.Path;否则 HTTPS 后端收到 HTTP URL 会 301 跳转,req.Host不显式赋值会导致 Nginx 或云 LB 拒绝请求 -
Transport:必须自定义,http.DefaultTransport默认不设MaxIdleConnsPerHost,高并发下 fd 耗尽;要配DialContext.Timeout(建议 5s)、IdleConnTimeout(建议 30s)、MaxIdleConnsPerHost(建议 100) -
ErrorHandler:默认直接WriteString错误文本,前端收不到标准 JSON;必须手动设Content-Type: application/json、状态码(如http.StatusBadGateway),并用json.NewEncoder输出结构化错误
真实客户端 IP 怎么透传才可靠
只取 req.RemoteAddr 是错的——它在有 LB 或 CDN 时返回的是中间节点地址;也不该无脑信任 X-Forwarded-For 最左 IP,得过滤私有网段。
- 写个
realIP(req *http.Request)函数:优先读X-Real-IP;若空,则从X-Forwarded-For按逗号分割,从右往左找第一个非私有 IP(如10.0.0.0/8、172.16.0.0/12、192.168.0.0/16) - 在
Director里调用:req.Header.Set("X-Real-IP", realIP(req)),同时设X-Forwarded-Proto(根据req.TLS != nil判断) - 务必删掉 hop-by-hop header:
req.Header.Del("Connection")、req.Header.Del("Keep-Alive")、req.Header.Del("Proxy-Authenticate"),否则后端可能解析异常
路径重写为什么不能用正则替换
比如把 /api/v1/users → /users,用 strings.Replace 或第三方 path rewrite 库,容易把 /api/v1/user-profile 错切成 /user-profile,破坏语义边界。
立即学习“go语言免费学习笔记(深入)”;
- 严格前缀匹配:用
strings.HasPrefix(req.URL.Path, "/api/v1")判断,再用strings.TrimPrefix(req.URL.Path, "/api/v1")截取 - 别碰
req.URL.EscapedPath()—— 它含原始编码,直接赋值会导致路径解码错误;应操作req.URL.Path后再调url.PathEscape(如有必要) - WebSocket 流量必须提前拦截:检查
req.Header.Get("Upgrade") == "websocket"和req.Header.Get("Connection"),满足则跳过proxy.ServeHTTP,避免httputil默认逻辑丢掉 Upgrade 头
聚合多个下游时,context 和结构体怎么防坑
聚合不是并发发请求那么简单;一个慢接口拖垮全部、空字段和零值分不清、goroutine 泄漏,都是高频线上问题。
- 每个下游请求必须绑定独立
context.WithTimeout:ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second),并在 goroutine 内defer cancel() - 用
sync.WaitGroup等待所有 goroutine 完成,别用time.After粗暴兜底;http.Client必须是自定义实例,不能用http.DefaultClient - 响应结构体字段全用指针类型:
*string、*int64、*bool;JSON 反序列化时缺失字段为nil,可明确区分“未返回”和“返回空字符串”
真正难的不是转发逻辑本身,而是 Transport 连接池参数与后端 keep-alive timeout 的对齐、Director 中 hop-by-hop header 的清理时机、以及 ModifyResponse 里 Location 头的相对化处理——这些细节不验证到压测数据,上线后只会以 CPU 持续 90%+ 或间歇性 30s 超时的形式反扑。


















