跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

Go 语言实现一个简单的 API 网关

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

相关文章