直接用net/http因其足够满足简单网关的路由转发与基础中间件需求,避免第三方库的冗余配置与隐式行为;通过http.NewServeMux+函数式中间件链即可精准控制流向,兼顾轻量、可控与调试便利。
为什么直接用
而不用第三方网关库
因为“简单 API 网关”的核心诉求是路由转发 + 基础中间件(如鉴权、日志),不是服务发现或熔断。Go 自带的
完全够用,加一层
或直接用
组合就能控制流向,避免引入
或
这类重型依赖带来的配置负担和隐式行为。
常见错误是过早抽象:比如一上来就设计插件系统、YAML 配置加载、动态规则热更新——这些在初期不仅没带来灵活性,反而让路由逻辑分散、调试困难。
用
就能做路径前缀匹配,满足大部分代理需求
所有中间件用函数链式包装
,不依赖框架生命周期
后端服务地址写死在 map 或结构体里,启动时校验可用性,比运行时解析配置更可控
如何安全地做反向代理而不暴露内部服务细节
Go 标准库的
是起点,但它默认透传所有请求头(包括
、
),且不自动改写响应体里的绝对 URL。不处理这些,网关会变成“透明通道”,后端服务可能误判客户端 IP,或返回指向自己端口的跳转链接。
关键动作是定制
和重写
:
中必须清除原始
头,设为后端地址的
;否则后端可能拒绝请求(尤其启用了 Host 白名单)
手动设置
和
,供后端识别真实客户端和协议
里检查
响应头,把
改成
(相对路径)或网关对外域名
务必调用
,否则连接不会复用,压测时很快耗尽文件描述符
怎么给不同路由加不同中间件(比如 /admin 需要 JWT,/public 不需要)
不能全局注册中间件,得按路由粒度组合。最直白的方式是为每个路由路径显式构造 handler 链:原始 handler → 日志 → 鉴权(条件跳过)→ 代理。
典型陷阱是中间件顺序写反:比如把鉴权放在日志之后没问题,但如果把日志放在代理之后,就只能记录到网关自身的 5xx 错误,收不到后端返回的状态码。
定义中间件函数类型:
,保持可组合性
对需要鉴权的路径,用
;对公开路径,直接用
注意中间件里不要提前
或
,否则后续 handler 会 panic:”http: multiple response.WriteHeader calls”
JWT 验证失败时,应直接
并 return,别再调用 next.ServeHTTP
示例路由注册:
为什么健康检查接口不能只 ping 后端服务端口
网关自身健康状态 ≠ 后端服务可达。如果只用
检查后端端口,会忽略后端进程卡死、DB 连接池耗尽、或路由配置错误等场景。真正有效的健康检查必须走完整请求链路。
但也不能每次请求都发一次 HTTP 探针——性能太差。折中做法是:后台 goroutine 定期(如 10 秒)对每个上游服务发一个轻量
,结果缓存在内存 map 中;主请求路径里只查这个 map。
缓存需加读写锁(
),避免并发读写 panic
探针超时设为 2 秒以内,避免拖慢整个健康检查周期
失败连续 3 次才标记为 down,防止网络抖动误判
接口返回值建议包含各上游状态,方便运维 curl 一眼看清
最容易被忽略的是:健康检查的请求头要和真实流量一致(比如带
),否则某些后端服务会因 header 校验失败而返回 403。
net/httpnet/httphttp.ServeMuxhttp.Handlergorilla/muxtraefikhttp.NewServeMux()http.Handlerhttputil.NewSingleHostReverseProxyX-Forwarded-ForUser-AgentDirectorModifyResponseDirectorHostHostX-Real-IPX-Forwarded-ProtoModifyResponseLocationhttp://localhost:8081/xxx/xxxresp.Body.Close()proxy := httputil.NewSingleHostReverseProxy(u)
proxy.Director = func(req *http.Request) {
req.Header.Set("X-Real-IP", realIP(req))
req.Header.Set("X-Forwarded-Proto", "https") // 根据 TLS 状态动态判断
req.Host = u.Host
req.URL.Scheme = u.Scheme
req.URL.Host = u.Host
}
proxy.ModifyResponse = func(resp *http.Response) error {
if loc := resp.Header.Get("Location"); loc != "" {
if url, err := url.Parse(loc); err == nil && !url.IsAbs() {
resp.Header.Set("Location", url.String())
}
}
return nil
}func(http.Handler) http.HandlerauthMiddleware(proxyHandler)proxyHandlerwriteHeaderWritehttp.Error(w, "Unauthorized", http.StatusUnauthorized)mux := http.NewServeMux()
mux.Handle("/api/", loggingMiddleware(authMiddleware(apiProxy)))
mux.Handle("/health", healthHandler)
mux.Handle("/public/", publicProxy)net.DialTimeoutHEAD /healthsync.RWMutex/healthX-Forwarded-For