Skip to content

API 网关模式

本篇是 Go 微服务系列的第五篇。当微服务数量增多后,让前端直接调用几十个分散的服务是不现实的——认证、限流、路由、监控都需要在统一入口完成。API 网关(API Gateway)正是承担这一职责的核心组件。本篇将讲解网关的职责、用 httputil.ReverseProxy 实现反向代理、基于 Gin 构建功能完整的网关、网关层认证、请求聚合与协议转换,并简要介绍 Kong 和 Traefik。

一、API 网关的职责

1. 为什么需要网关

没有网关时,前端要直接对接每个微服务:

前端 ──► 用户服务
前端 ──► 订单服务
前端 ──► 商品服务
前端 ──► 支付服务

问题:

  • 每个服务都要实现认证、限流、日志、CORS,重复造轮子。
  • 服务地址暴露给前端,增加耦合。
  • 跨域、聚合请求难以处理。
  • 安全风险:内部服务直接暴露给外网。

引入网关后:

前端 ──► API Gateway ──► 用户服务
                     ──► 订单服务
                     ──► 商品服务
                     ──► 支付服务

2. 网关的核心职责

  • 路由转发:把请求按路径转发到对应的后端服务。
  • 负载均衡:在多个服务实例间分发请求。
  • 认证鉴权:统一校验 JWT、API Key,避免每个服务重复实现。
  • 限流熔断:保护后端服务不被流量打垮。
  • 协议转换:对外 REST,对内 gRPC。
  • 请求聚合:把对多个服务的调用合并成一个对外接口。
  • 缓存:对幂等请求做缓存,减轻后端压力。
  • 监控日志:统一的访问日志、指标埋点。
  • CORS:统一处理跨域。

3. 网关 vs 服务网格(Service Mesh)

维度API 网关服务网格(如 Istio)
位置南北向(外部进入)东西向(服务间)
形态集中式部署每个服务一个 sidecar
主要职责对外入口、聚合、鉴权服务间通信治理、可观测性
互补性可与 Service Mesh 共存同左

成熟的架构通常网关 + Mesh 共存:网关处理南北向,Mesh 处理东西向。

二、反向代理实现:httputil.ReverseProxy

1. 标准库的反向代理

Go 标准库 net/http/httputil 提供了 ReverseProxy,几行代码即可实现一个反向代理:

go
package main

import (
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
)

func main() {
	target, _ := url.Parse("http://localhost:8081")
	proxy := httputil.NewSingleHostReverseProxy(target)

	log.Println("proxy on :8080 -> :8081")
	log.Fatal(http.ListenAndServe(":8080", proxy))
}

访问 http://localhost:8080/anything 会被原样转发到 http://localhost:8081/anything

2. 自定义 ReverseProxy

NewSingleHostReverseProxy 是简化封装,实际项目要自定义更多行为:

go
package main

import (
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"
)

type DynamicProxy struct {
	routes map[string]*httputil.ReverseProxy
}

func NewDynamicProxy() *DynamicProxy {
	p := &DynamicProxy{routes: make(map[string]*httputil.ReverseProxy)}

	// 用户服务:/api/users/* -> http://localhost:8081
	p.addRoute("/api/users", "http://localhost:8081")
	// 订单服务:/api/orders/* -> http://localhost:8082
	p.addRoute("/api/orders", "http://localhost:8082")
	// 商品服务:/api/products/* -> http://localhost:8083
	p.addRoute("/api/products", "http://localhost:8083")
	return p
}

func (p *DynamicProxy) addRoute(prefix, target string) {
	u, err := url.Parse(target)
	if err != nil {
		log.Fatal(err)
	}
	proxy := httputil.NewSingleHostReverseProxy(u)
	// 自定义 Director:修改请求
	originalDirector := proxy.Director
	proxy.Director = func(req *http.Request) {
		originalDirector(req)
		req.Header.Set("X-Forwarded-Host", req.Host)
		req.Header.Set("X-Real-IP", req.RemoteAddr)
	}
	// 自定义 ModifyResponse:修改响应
	proxy.ModifyResponse = func(resp *http.Response) error {
		resp.Header.Set("X-Gateway", "go-gateway")
		return nil
	}
	// 自定义 ErrorHandler:后端不可用时的兜底
	proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
		log.Printf("upstream error: %v", err)
		http.Error(w, `{"code":503,"message":"service unavailable"}`, http.StatusServiceUnavailable)
	}
	// 超时
	proxy.Transport = &http.Transport{
		IdleConnTimeout:       30 * time.Second,
		ResponseHeaderTimeout: 5 * time.Second,
	}
	p.routes[prefix] = proxy
}

func (p *DynamicProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	for prefix, proxy := range p.routes {
		if strings.HasPrefix(r.URL.Path, prefix) {
			proxy.ServeHTTP(w, r)
			return
		}
	}
	http.NotFound(w, r)
}

func main() {
	p := NewDynamicProxy()
	log.Println("gateway on :8080")
	log.Fatal(http.ListenAndServe(":8080", p))
}

3. 关键扩展点

ReverseProxy 提供了多个扩展点:

字段作用典型场景
Director修改请求改路径、加 Header
ModifyResponse修改响应加统一响应头、脱敏
ErrorHandler后端错误处理兜底响应、告警
Transport自定义 HTTP Transport连接池、超时、TLS
BufferPool复用缓冲区减少内存分配

三、基于 Gin 的 API 网关

1. 整体结构

go
package main

import (
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
)

type Route struct {
	Prefix      string
	Target      string
	StripPrefix bool
}

type Gateway struct {
	routes map[string]*httputil.ReverseProxy
}

func NewGateway(routes []Route) *Gateway {
	g := &Gateway{routes: make(map[string]*httputil.ReverseProxy)}
	for _, r := range routes {
		u, err := url.Parse(r.Target)
		if err != nil {
			log.Fatal(err)
		}
		proxy := httputil.NewSingleHostReverseProxy(u)
		originalDirector := proxy.Director
		proxy.Director = func(req *http.Request) {
			originalDirector(req)
			if r.StripPrefix {
				req.URL.Path = strings.TrimPrefix(req.URL.Path, r.Prefix)
				if !strings.HasPrefix(req.URL.Path, "/") {
					req.URL.Path = "/" + req.URL.Path
				}
			}
		}
		proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
			log.Printf("[gateway] upstream error: %v", err)
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusServiceUnavailable)
			_, _ = w.Write([]byte(`{"code":503,"message":"service unavailable"}`))
		}
		g.routes[r.Prefix] = proxy
	}
	return g
}

func (g *Gateway) Handler() gin.HandlerFunc {
	return func(c *gin.Context) {
		path := c.Request.URL.Path
		for prefix, proxy := range g.routes {
			if strings.HasPrefix(path, prefix) {
				proxy.ServeHTTP(c.Writer, c.Request)
				return
			}
		}
		c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "no route"})
	}
}

func main() {
	gw := NewGateway([]Route{
		{Prefix: "/api/users", Target: "http://localhost:8081", StripPrefix: true},
		{Prefix: "/api/orders", Target: "http://localhost:8082", StripPrefix: true},
		{Prefix: "/api/products", Target: "http://localhost:8083", StripPrefix: true},
	})

	r := gin.New()
	r.Use(gin.Recovery(), gin.Logger())
	r.Any("/api/*any", gw.Handler())

	r.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           r,
		ReadHeaderTimeout: 5 * time.Second,
	}
	log.Println("gateway on :8080")
	log.Fatal(srv.ListenAndServe())
}

四、路由转发与负载均衡

1. 多实例负载均衡

实际生产中每个服务都有多个实例,网关需要在实例间做负载均衡。把单一 *ReverseProxy 升级为多实例 + 选择的逻辑:

go
package main

import (
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"sync/atomic"
)

type Instance struct {
	Addr string
}

type RoundRobinBalancer struct {
	instances []*url.URL
	idx       uint32
}

func NewRoundRobinBalancer(addrs []string) *RoundRobinBalancer {
	us := make([]*url.URL, 0, len(addrs))
	for _, a := range addrs {
		u, err := url.Parse(a)
		if err != nil {
			log.Fatal(err)
		}
		us = append(us, u)
	}
	return &RoundRobinBalancer{instances: us}
}

func (b *RoundRobinBalancer) Next() *url.URL {
	n := uint32(len(b.instances))
	i := atomic.AddUint32(&b.idx, 1) % n
	return b.instances[i]
}

type LBProxy struct {
	balancer *RoundRobinBalancer
	proxy    *httputil.ReverseProxy
}

func NewLBProxy(addrs []string) *LBProxy {
	b := NewRoundRobinBalancer(addrs)
	p := &LBProxy{balancer: b}
	p.proxy = &httputil.ReverseProxy{
		Director: func(req *http.Request) {
			target := b.Next()
			req.URL.Scheme = target.Scheme
			req.URL.Host = target.Host
			req.Header.Set("X-Forwarded-Host", req.Host)
		},
	}
	return p
}

func (p *LBProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	p.proxy.ServeHTTP(w, r)
}

func main() {
	proxy := NewLBProxy([]string{
		"http://localhost:8081",
		"http://localhost:8082",
		"http://localhost:8083",
	})
	log.Println("lb proxy on :8080")
	log.Fatal(http.ListenAndServe(":8080", proxy))
}

2. 集成服务发现

把上面的写死地址换成从 Consul / Etcd 动态拉取,再加上健康检查,就是一个完整的生产级负载均衡器。这部分可以参考第 2 篇的服务发现实现。

五、网关层认证中间件

1. JWT 认证

网关统一做认证,后端服务无需重复实现:

go
package main

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
)

type JWTAuth struct {
	secret []byte
	// 不需要鉴权的路径
	publicPaths map[string]bool
}

func NewJWTAuth(secret string) *JWTAuth {
	return &JWTAuth{
		secret:      []byte(secret),
		publicPaths: map[string]bool{"/api/auth/login": true, "/api/auth/register": true, "/health": true},
	}
}

func (a *JWTAuth) Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		if a.publicPaths[c.Request.URL.Path] {
			c.Next()
			return
		}
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "missing token"})
			return
		}
		parts := strings.SplitN(authHeader, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid auth header"})
			return
		}
		tokenStr := parts[1]
		token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
			if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, jwt.ErrSignatureInvalid
			}
			return a.secret, nil
		})
		if err != nil || !token.Valid {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid token"})
			return
		}
		// 把 claims 传给下游
		if claims, ok := token.Claims.(jwt.MapClaims); ok {
			c.Set("user_id", claims["sub"])
			c.Request.Header.Set("X-User-ID", claims["sub"].(string))
		}
		c.Next()
	}
}

// IssueToken 签发 token(登录接口使用)
func (a *JWTAuth) IssueToken(userID string) (string, error) {
	claims := jwt.MapClaims{
		"sub": userID,
		"iss": "go-gateway",
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(a.secret)
}

func main() {
	auth := NewJWTAuth("super-secret-key")

	r := gin.New()
	r.Use(gin.Recovery(), gin.Logger())
	r.Use(auth.Middleware())

	// 登录接口(public)
	r.POST("/api/auth/login", func(c *gin.Context) {
		// 模拟登录成功
		token, _ := auth.IssueToken("user-123")
		c.JSON(http.StatusOK, gin.H{"token": token})
	})

	// 受保护接口(演示)
	r.GET("/api/me", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"user_id": c.GetString("user_id")})
	})

	_ = r.Run(":8080")
}

后端服务从 X-User-ID header 读取用户身份即可,不必再做 JWT 校验。

2. API Key 认证

适合内部服务或第三方对接:

go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type APIKeyAuth struct {
	keys map[string]string // key -> owner
}

func NewAPIKeyAuth(keys map[string]string) *APIKeyAuth {
	return &APIKeyAuth{keys: keys}
}

func (a *APIKeyAuth) Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		key := c.GetHeader("X-API-Key")
		if key == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "missing api key"})
			return
		}
		owner, ok := a.keys[key]
		if !ok {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "invalid api key"})
			return
		}
		c.Set("api_owner", owner)
		c.Next()
	}
}

func main() {
	auth := NewAPIKeyAuth(map[string]string{
		"abc-123": "internal-app",
		"xyz-789": "partner-a",
	})
	r := gin.New()
	r.Use(auth.Middleware())
	r.GET("/data", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"owner": c.GetString("api_owner")})
	})
	_ = r.Run(":8080")
}

六、请求聚合与协议转换

1. 请求聚合

前端一个页面常常需要多个接口的数据,例如「我的订单页」需要:用户信息、订单列表、商品详情、物流状态。网关可以聚合为一次请求:

go
package main

import (
	"encoding/json"
	"io"
	"net/http"
	"sync"

	"github.com/gin-gonic/gin"
)

type Aggregator struct {
	client *http.Client
}

func NewAggregator() *Aggregator {
	return &Aggregator{client: &http.Client{}}
}

func (a *Aggregator) fetch(url string) (map[string]interface{}, error) {
	resp, err := a.client.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	var m map[string]interface{}
	if err := json.Unmarshal(body, &m); err != nil {
		return nil, err
	}
	return m, nil
}

// OrderDetail 聚合订单详情:用户 + 订单 + 商品
func (a *Aggregator) OrderDetail(c *gin.Context) {
	orderID := c.Param("id")
	var wg sync.WaitGroup
	wg.Add(3)

	var (
		user    map[string]interface{}
		order   map[string]interface{}
		product map[string]interface{}
		errs    [3]error
	)

	go func() {
		defer wg.Done()
		user, errs[0] = a.fetch("http://localhost:8081/users/1")
	}()
	go func() {
		defer wg.Done()
		order, errs[1] = a.fetch("http://localhost:8082/orders/" + orderID)
	}()
	go func() {
		defer wg.Done()
		product, errs[2] = a.fetch("http://localhost:8083/products/1")
	}()

	wg.Wait()
	c.JSON(http.StatusOK, gin.H{
		"user":    user,
		"order":   order,
		"product": product,
	})
}

func main() {
	agg := NewAggregator()
	r := gin.Default()
	r.GET("/aggr/orders/:id", agg.OrderDetail)
	_ = r.Run(":8080")
}

三个后端请求并发执行,整体延迟约等于最慢的那个请求,而不是三者之和。

2. 协议转换(REST → gRPC)

网关接收 REST 请求,调用后端的 gRPC 服务,再把结果转成 JSON 返回。这种模式叫 gRPC-Gateway,社区有现成工具 grpc-ecosystem/grpc-gateway

手工实现一个简化版骨架:

go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
)

// 假设有一个 gRPC 客户端(实际通过 protoc 生成)
type GRPCUserClient struct{}

func (c *GRPCUserClient) GetUser(ctx context.Context, id int64) (map[string]interface{}, error) {
	// 真实场景:client.GetUser(ctx, &userv1.GetUserRequest{Id: id})
	return map[string]interface{}{
		"id":   id,
		"name": fmt.Sprintf("user-%d", id),
	}, nil
}

type REST2GRPC struct {
	grpc *GRPCUserClient
}

func (h *REST2GRPC) GetUser(c *gin.Context) {
	idStr := c.Param("id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "invalid id"})
		return
	}
	ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
	defer cancel()
	user, err := h.grpc.GetUser(ctx, id)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
		return
	}
	// gRPC 响应转 JSON
	c.JSON(http.StatusOK, user)
}

func main() {
	h := &REST2GRPC{grpc: &GRPCUserClient{}}
	r := gin.Default()
	r.GET("/v1/users/:id", h.GetUser)
	_ = r.Run(":8080")
}

七、Kong / Traefik 简介

1. Kong

Kong 基于 OpenResty(Nginx + Lua),性能极高,插件生态丰富:

  • 核心特性:路由、负载均衡、认证(JWT、OAuth2、Basic Auth)、限流、日志、监控。
  • 数据库:PostgreSQL / Cassandra / 无DB 模式。
  • 插件化:用 Lua / Go / JavaScript 写插件。
  • 管理 API:RESTful 管理 API,便于自动化。
  • Konga:第三方 Web UI。

适合:大流量、需要丰富插件、运维能力强的团队。

2. Traefik

Traefik 是 Go 写的现代反向代理,特点是动态配置:

  • 自动服务发现:与 Docker、Kubernetes、Consul、Etcd 集成,自动感知新服务。
  • Let's Encrypt 自动续签:原生支持 HTTPS 证书自动申请。
  • 配置即代码:YAML/TOML 配置,也支持标签配置。
  • Dashboard:内置可视化界面。
  • 性能:基于 Go net/http,性能不如 Kong,但够用。

适合:容器化部署、配置变更频繁的团队。

3. APISIX

国内开源,基于 Nginx + etcd,性能接近 Kong,动态路由更灵活,中文文档完善,在国内采用率很高。

4. 自研 vs 现成

维度自研 (Gin + ReverseProxy)Kong / Traefik / APISIX
灵活性极高,业务逻辑无缝集成中等,需写插件
上手Go 开发者最快需学习平台
生态自己积累丰富插件 + 社区
性能取决于实现Kong / APISIX 更高
推荐业务强相关网关(聚合、协议转换)通用 API 流量入口

常见做法:边缘用 Kong/APISIX 做流量入口,内部用自研网关做业务聚合

八、完整示例:微服务 API 网关

下面给出一个集成了路由、负载均衡、JWT 认证、限流、聚合的完整网关示例(为了独立编译,限流用简化版):

go
package main

import (
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"sync/atomic"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
)

// === 负载均衡器 ===

type LB struct {
	targets []*url.URL
	idx     uint32
}

func NewLB(addrs []string) *LB {
	ts := make([]*url.URL, 0, len(addrs))
	for _, a := range addrs {
		u, err := url.Parse(a)
		if err != nil {
			log.Fatal(err)
		}
		ts = append(ts, u)
	}
	return &LB{targets: ts}
}

func (lb *LB) Next() *url.URL {
	if len(lb.targets) == 0 {
		return nil
	}
	i := atomic.AddUint32(&lb.idx, 1) % uint32(len(lb.targets))
	return lb.targets[i]
}

// === 路由配置 ===

type RouteConfig struct {
	Prefix   string
	Targets  []string
	NeedAuth bool
}

type Gateway struct {
	routes []RouteConfig
	lbs    map[string]*LB
}

func NewGateway(routes []RouteConfig) *Gateway {
	g := &Gateway{routes: routes, lbs: make(map[string]*LB)}
	for _, r := range routes {
		g.lbs[r.Prefix] = NewLB(r.Targets)
	}
	return g
}

func (g *Gateway) Handler() gin.HandlerFunc {
	return func(c *gin.Context) {
		path := c.Request.URL.Path
		for _, r := range g.routes {
			if strings.HasPrefix(path, r.Prefix) {
				if r.NeedAuth {
					if c.GetString("user_id") == "" {
						c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
						return
					}
				}
				lb := g.lbs[r.Prefix]
				target := lb.Next()
				if target == nil {
					c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "no upstream"})
					return
				}
				proxy := httputil.NewSingleHostReverseProxy(target)
				origDirector := proxy.Director
				proxy.Director = func(req *http.Request) {
					origDirector(req)
					req.Header.Set("X-Forwarded-Host", req.Host)
					if uid := c.GetString("user_id"); uid != "" {
						req.Header.Set("X-User-ID", uid)
					}
				}
				proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
					log.Printf("[gw] upstream err: %v", err)
					w.Header().Set("Content-Type", "application/json")
					w.WriteHeader(http.StatusServiceUnavailable)
					_, _ = w.Write([]byte(`{"code":503,"message":"service unavailable"}`))
				}
				proxy.ServeHTTP(c.Writer, c.Request)
				return
			}
		}
		c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "no route"})
	}
}

// === JWT 认证 ===

type JWTAuth struct {
	secret      []byte
	publicPaths map[string]bool
}

func NewJWTAuth(secret string, public []string) *JWTAuth {
	m := make(map[string]bool)
	for _, p := range public {
		m[p] = true
	}
	return &JWTAuth{secret: []byte(secret), publicPaths: m}
}

func (a *JWTAuth) Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		if a.publicPaths[c.Request.URL.Path] {
			c.Next()
			return
		}
		h := c.GetHeader("Authorization")
		if !strings.HasPrefix(h, "Bearer ") {
			c.Next() // 让下游路由判断是否需要鉴权
			return
		}
		tokenStr := strings.TrimPrefix(h, "Bearer ")
		token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
			return a.secret, nil
		})
		if err == nil && token.Valid {
			if claims, ok := token.Claims.(jwt.MapClaims); ok {
				if sub, ok := claims["sub"].(string); ok {
					c.Set("user_id", sub)
				}
			}
		}
		c.Next()
	}
}

func (a *JWTAuth) Issue(userID string) (string, error) {
	t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"sub": userID,
		"iss": "go-gateway",
		"exp": time.Now().Add(2 * time.Hour).Unix(),
	})
	return t.SignedString(a.secret)
}

// === 限流(简化令牌桶) ===

type RateLimiter struct {
	tokens chan struct{}
}

func NewRateLimiter(rate int, burst int) *RateLimiter {
	rl := &RateLimiter{tokens: make(chan struct{}, burst)}
	for i := 0; i < burst; i++ {
		rl.tokens <- struct{}{}
	}
	go func() {
		t := time.NewTicker(time.Second / time.Duration(rate))
		for range t.C {
			select {
			case rl.tokens <- struct{}{}:
			default:
			}
		}
	}()
	return rl
}

func (rl *RateLimiter) Allow() bool {
	select {
	case <-rl.tokens:
		return true
	default:
		return false
	}
}

func RateLimitMiddleware(rl *RateLimiter) gin.HandlerFunc {
	return func(c *gin.Context) {
		if !rl.Allow() {
			c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"code": 429, "message": "rate limited"})
			return
		}
		c.Next()
	}
}

func main() {
	gw := NewGateway([]RouteConfig{
		{Prefix: "/api/auth", Targets: []string{"http://localhost:8081"}, NeedAuth: false},
		{Prefix: "/api/users", Targets: []string{"http://localhost:8082", "http://localhost:8083"}, NeedAuth: true},
		{Prefix: "/api/orders", Targets: []string{"http://localhost:8084"}, NeedAuth: true},
	})
	auth := NewJWTAuth("gateway-secret", []string{"/api/auth/login", "/health"})
	limiter := NewRateLimiter(100, 200)

	r := gin.New()
	r.Use(gin.Recovery(), gin.Logger())
	r.Use(RateLimitMiddleware(limiter))
	r.Use(auth.Middleware())

	// 登录接口(签发 token)
	r.POST("/api/auth/login", func(c *gin.Context) {
		token, _ := auth.Issue("user-001")
		c.JSON(http.StatusOK, gin.H{"token": token})
	})

	// 转发路由
	r.Any("/api/*any", gw.Handler())

	r.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           r,
		ReadHeaderTimeout: 5 * time.Second,
	}
	log.Println("gateway on :8080")
	log.Fatal(srv.ListenAndServe())
}

九、小结

本篇我们学习了 API 网关模式:

  • 核心职责:路由转发、负载均衡、认证、限流、协议转换、请求聚合、监控日志。
  • 反向代理:标准库 httputil.ReverseProxy + 自定义 Director/ModifyResponse/ErrorHandler。
  • Gin 网关:用 RouterGroup + ReverseProxy 组合,灵活可扩展。
  • 负载均衡:把单一 target 升级为多实例 + 选择策略(轮询、随机、加权)。
  • 认证中间件:网关统一做 JWT / API Key 校验,下游通过 header 拿到用户身份。
  • 请求聚合:并发调用多个后端接口,合并响应,降低前端请求数。
  • 协议转换:gRPC-Gateway 模式,对外 REST 对内 gRPC。
  • 选型:自研适合业务强相关场景,Kong / Traefik / APISIX 适合通用流量入口。

下一篇我们将进入分布式配置中心,学习如何用 Viper 管理配置、从 Consul KV 读取配置、实现配置热更新。

延伸阅读