需自定义http.Handler实现可插拔HTTP代理:解析→权限检查(Handler入口处验Token)→延迟加载后端(用lazy.Group支持重试)→转发(复制Header、配专用Transport防泄漏)。

Go 里怎么写一个可插拔的 HTTP 代理(不是 net/http/httputil.ReverseProxy 那种简单转发)
真要加权限控制和延迟加载,直接套 ReverseProxy 不够用——它内部把连接、header、body 全包死了,你插不进校验逻辑,也做不到按需初始化后端服务实例。
得自己实现 http.Handler,把请求生命周期拆开:解析 → 权限检查 → 目标发现(延迟加载)→ 转发 → 响应处理。
- 权限检查必须在读取
req.Body前完成,否则 body 可能被消耗掉,后续转发失败 - 延迟加载目标服务时,别在每次请求里 new struct,用
sync.Once或懒初始化 map +sync.RWMutex - 转发前记得复制 header(
req.Header.Clone()),否则并发下可能被其他 goroutine 修改
// 示例:权限检查 + 懒加载后端
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !p.hasPermission(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
backend := p.getBackend(r.Host) // 内部用 sync.Once 初始化
proxyRequest(w, r, backend.Addr)
}
Go 的延迟加载为什么不能只靠 sync.Once?什么时候该用 lazy.Group
sync.Once 确实能防重复初始化,但它不处理错误重试、超时、依赖隔离——比如后端服务启动失败,你总不能让所有请求卡死在 Once.Do() 里等它修好。
真实场景中,代理要支持“某个上游暂时不可用,先返回降级响应,后台继续重试初始化”,这时候得用 lazy.Group(来自 golang.org/x/sync)或自己封装带重试的懒加载器。
立即学习“go语言免费学习笔记(深入)”;
-
sync.Once是一锤子买卖,成功或 panic 后就永远不会再执行 -
lazy.Group允许你定义初始化函数返回(T, error),失败后下次调用还能重试 - 如果后端是数据库连接池,延迟加载还涉及连接健康检查,不能只看“是否初始化过”
HTTP 代理里做权限控制,token 解析放哪一步最稳?
别在 RoundTrip 阶段解析 token——那是 http.Transport 层的事,你根本没机会拦截原始请求头;也别在转发后验签,那已经晚了。
唯一安全的位置是自定义 http.Handler 的入口,在调用 backend.RoundTrip(req) 前,从 r.Header.Get("Authorization") 提取并验证。
- JWT 验证务必用
github.com/golang-jwt/jwt/v5,别手撕 base64 解码——时区、nbf/exp 校验、kid 匹配都容易漏 - 如果权限依赖用户角色查 DB,记得设 context 超时,避免代理卡住(比如 DB 慢查询拖垮整个代理)
- 别把 token 原样透传给后端,除非明确需要;更常见的是提取 user_id 放到
X-User-IDheader 转发
为什么 Go 代理里用 http.DefaultTransport 容易出连接泄漏?
默认 Transport 的 MaxIdleConnsPerHost 是 2,代理高频转发时很快打满,新请求排队,表现就是“偶发超时”或“大量 TIME_WAIT”。这不是 bug,是配置没动过。
- 必须显式配置
&http.Transport{MaxIdleConnsPerHost: 100},否则每秒几十个请求就能堵住 - 如果代理要转发长连接(如 WebSocket),还得设
IdleConnTimeout和KeepAlive,不然空闲连接永远不关 - 别复用全局
http.DefaultClient做转发——它的 Transport 是共享的,你改配置会影响其他模块
复杂点在于:权限控制逻辑可能要调第三方鉴权服务,那个 client 也得单独配 Transport,和代理转发的 client 隔开。两个用途混用一套连接池,很容易互相拖垮。










