Appearance
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 读取配置、实现配置热更新。
延伸阅读:
- 微服务模式:API Gateway
- httputil 文档:https://pkg.go.dev/net/http/httputil
- Kong 文档:https://docs.konghq.com/
- Traefik 文档:https://doc.traefik.io/