Go 实现 API 网关——路由、限流、鉴权、日志的轻量级网关实现
Go 实现 API 网关——路由、限流、鉴权、日志的轻量级网关实现
适读人群:Go 后端工程师、想自己实现 API 网关或理解其原理的开发者 | 阅读时长:约 19 分钟 | 核心价值:用 Go 标准库 + 少量依赖实现生产可用的轻量级 API 网关
去年有个做 ToB SaaS 的团队,他们的后端由十几个微服务组成,最初图快直接把所有服务暴露在公网,没有统一的入口。后来 API key 泄露了一次,攻击者爬走了大量数据。他们想加一层统一的 API 网关来做认证和限流。
市面上的网关(Kong、APISIX、Envoy)功能很全,但对他们这个规模来说太重了——要维护一个独立的网关集群,配置学习成本高。我建议他们用 Go 实现一个"恰好够用"的轻量级网关,核心功能:路由转发、JWT 鉴权、限流、访问日志。
这篇文章把那个网关的核心实现整理出来。
架构设计
客户端
↓
API Gateway (Go)
├── 中间件链:日志 → 限流 → 鉴权 → 路由
↓
后端服务(user-service / order-service / ...)网关的核心是 中间件链,每个请求依次经过各个中间件,最后由路由器转发到后端服务。
完整实现
核心结构
package gateway
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
// Route 路由规则
type Route struct {
Prefix string // 路径前缀,如 "/api/users"
Target string // 目标服务地址,如 "http://user-service:8080"
StripPrefix bool // 是否去掉前缀
AuthRequired bool // 是否需要鉴权
RateLimit int // 每秒请求数限制(0表示不限)
}
// Gateway API 网关
type Gateway struct {
routes []*Route
rateLimiters sync.Map // key: clientIP, value: *rate.Limiter
jwtSecret []byte
}
func NewGateway(jwtSecret string) *Gateway {
return &Gateway{
jwtSecret: []byte(jwtSecret),
}
}
func (g *Gateway) AddRoute(route *Route) {
g.routes = append(g.routes, route)
}
// ServeHTTP 实现 http.Handler
func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 构建中间件链
handler := g.chain(
g.logMiddleware,
g.rateLimitMiddleware,
g.authMiddleware,
g.proxyHandler,
)
handler.ServeHTTP(w, r)
}
type Middleware func(http.Handler) http.Handler
func (g *Gateway) chain(middlewares ...interface{}) http.Handler {
// 最后一个是实际处理器
handler := middlewares[len(middlewares)-1].(func(http.ResponseWriter, *http.Request))
h := http.HandlerFunc(handler)
// 从外到内包裹中间件(倒序遍历)
for i := len(middlewares) - 2; i >= 0; i-- {
mw := middlewares[i].(Middleware)
h = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mw(h).ServeHTTP(w, r)
})
}
return h
}日志中间件
// ResponseWriter 包装,用于捕获状态码
type responseWriter struct {
http.ResponseWriter
statusCode int
written int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.written += n
return n, err
}
func (g *Gateway) logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(rw, r)
duration := time.Since(start)
log.Printf("%s %s %s %d %d %v %s",
clientIP(r),
r.Method,
r.URL.Path,
rw.statusCode,
rw.written,
duration,
r.Header.Get("X-Request-ID"),
)
})
}限流中间件(基于 IP 的令牌桶)
func (g *Gateway) getRateLimiter(ip string, rps int) *rate.Limiter {
key := fmt.Sprintf("%s:%d", ip, rps)
if v, ok := g.rateLimiters.Load(key); ok {
return v.(*rate.Limiter)
}
limiter := rate.NewLimiter(rate.Limit(rps), rps*2) // burst = 2x rps
g.rateLimiters.Store(key, limiter)
return limiter
}
func (g *Gateway) rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 找到匹配的路由配置
route := g.matchRoute(r.URL.Path)
if route == nil || route.RateLimit == 0 {
next.ServeHTTP(w, r)
return
}
ip := clientIP(r)
limiter := g.getRateLimiter(ip, route.RateLimit)
if !limiter.Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, `{"error":"too many requests"}`, http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}JWT 鉴权中间件
import (
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID string `json:"user_id"`
Scopes []string `json:"scopes"`
jwt.RegisteredClaims
}
func (g *Gateway) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := g.matchRoute(r.URL.Path)
if route == nil || !route.AuthRequired {
next.ServeHTTP(w, r)
return
}
// 从 Authorization header 提取 Bearer token
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
tokenStr := authHeader[7:]
// 解析并验证 JWT
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return g.jwtSecret, nil
})
if err != nil || !token.Valid {
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
return
}
// 把用户信息注入请求头,传给后端服务
r.Header.Set("X-User-ID", claims.UserID)
r.Header.Set("X-User-Scopes", strings.Join(claims.Scopes, ","))
next.ServeHTTP(w, r)
})
}反向代理处理器
func (g *Gateway) proxyHandler(w http.ResponseWriter, r *http.Request) {
route := g.matchRoute(r.URL.Path)
if route == nil {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
target, err := url.Parse(route.Target)
if err != nil {
http.Error(w, `{"error":"bad gateway config"}`, http.StatusInternalServerError)
return
}
proxy := httputil.NewSingleHostReverseProxy(target)
// 自定义错误处理
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Proxy error to %s: %v", route.Target, err)
http.Error(w, `{"error":"service unavailable"}`, http.StatusServiceUnavailable)
}
// 修改请求
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
// 去掉前缀
if route.StripPrefix && strings.HasPrefix(req.URL.Path, route.Prefix) {
req.URL.Path = req.URL.Path[len(route.Prefix):]
if req.URL.Path == "" {
req.URL.Path = "/"
}
}
// 添加网关标识 header
req.Header.Set("X-Forwarded-By", "api-gateway")
req.Header.Set("X-Real-IP", clientIP(r))
}
proxy.ServeHTTP(w, r)
}
// matchRoute 最长前缀匹配
func (g *Gateway) matchRoute(path string) *Route {
var matched *Route
for _, route := range g.routes {
if strings.HasPrefix(path, route.Prefix) {
if matched == nil || len(route.Prefix) > len(matched.Prefix) {
matched = route
}
}
}
return matched
}
func clientIP(r *http.Request) string {
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
parts := strings.Split(ip, ",")
return strings.TrimSpace(parts[0])
}
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
return ip
}使用示例
func main() {
gw := gateway.NewGateway(os.Getenv("JWT_SECRET"))
gw.AddRoute(&gateway.Route{
Prefix: "/api/users",
Target: "http://user-service:8080",
StripPrefix: false,
AuthRequired: true,
RateLimit: 100, // 每秒 100 请求/IP
})
gw.AddRoute(&gateway.Route{
Prefix: "/api/orders",
Target: "http://order-service:8080",
AuthRequired: true,
RateLimit: 50,
})
gw.AddRoute(&gateway.Route{
Prefix: "/public",
Target: "http://static-service:8080",
AuthRequired: false,
RateLimit: 200,
})
server := &http.Server{
Addr: ":8080",
Handler: gw,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Println("API Gateway listening on :8080")
log.Fatal(server.ListenAndServe())
}踩坑实录
踩坑 1:sync.Map 里的 limiter 无限增长,内存泄漏
现象:运行一段时间后,网关内存持续增长,分析发现是 rateLimiters map 里存了几十万个 IP 的 limiter。
原因:每个新 IP 都会创建一个 limiter,但没有清理机制。
解法:定期清理不活跃的 limiter。用带过期时间的缓存替代 sync.Map:
type limitEntry struct {
limiter *rate.Limiter
lastSeen time.Time
}
// 每5分钟清理超过10分钟没有请求的 limiter
func (g *Gateway) cleanupLimiters() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
threshold := time.Now().Add(-10 * time.Minute)
g.rateLimiters.Range(func(key, value interface{}) bool {
if entry, ok := value.(*limitEntry); ok {
if entry.lastSeen.Before(threshold) {
g.rateLimiters.Delete(key)
}
}
return true
})
}
}踩坑 2:httputil.ReverseProxy 不自动处理 WebSocket 升级
现象:后端某个服务用了 WebSocket,通过网关访问时连接无法建立。
原因:ReverseProxy 默认不处理 Upgrade: websocket 这类 HTTP 升级请求。
解法:检查 Upgrade header,对 WebSocket 请求用 TCP 隧道直接转发(类似 CONNECT 代理)。
踩坑 3:限流计数按 IP 不够精细,同一个公司 NAT 出口被限
现象:某个企业客户几十人同时用,他们的出口 IP 只有几个,被 IP 限流误伤。
解法:优先用 JWT 里的 user_id 做限流粒度,有 token 的请求按 user_id 限流,无 token 的请求才按 IP 限流:
limitKey := clientIP(r)
if userID := r.Header.Get("X-User-ID"); userID != "" {
limitKey = "user:" + userID
}