Announcement

👇Official Account👇

图片

Welcome to join the group & private message

Article first/tail QR code

Skip to content

如何实现 RESTful API 版本控制

API 版本控制是后端开发的重要课题。本文介绍多种版本控制策略,帮助你做出正确选择。

一、为什么需要 API 版本控制

1.1 版本控制的必要性

  • 向后兼容:旧版本客户端继续工作
  • 功能迭代:平滑引入新功能
  • Bug 修复:不影响现有用户
  • 弃用策略:优雅下线旧版本

1.2 版本控制策略对比

策略示例优点缺点
URL Path/v1/users直观、易缓存URL 冗长
HeaderAccept: v1URL 简洁不易测试
Query?version=v1灵活不标准
子域名v1.api.com独立部署配置复杂

二、URL Path 版本控制

2.1 基本实现

go
package main

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

func main() {
    r := gin.Default()
    
    // v1 路由组
    v1 := r.Group("/api/v1")
    {
        v1.GET("/users", getUsersV1)
        v1.GET("/users/:id", getUserV1)
        v1.POST("/users", createUserV1)
    }
    
    // v2 路由组
    v2 := r.Group("/api/v2")
    {
        v2.GET("/users", getUsersV2)
        v2.GET("/users/:id", getUserV2)
        v2.POST("/users", createUserV2)
    }
    
    r.Run(":8080")
}

2.2 处理程序实现

go
// V1 版本
type UserV1 struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func getUsersV1(c *gin.Context) {
    users := []UserV1{
        {ID: 1, Name: "Alice"},
        {ID: 2, Name: "Bob"},
    }
    c.JSON(200, users)
}

// V2 版本 - 新增字段
type UserV2 struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

func getUsersV2(c *gin.Context) {
    users := []UserV2{
        {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
        {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
    }
    c.JSON(200, users)
}

三、Header 版本控制

3.1 使用 Accept Header

go
func versionMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从 Accept Header 获取版本
        accept := c.GetHeader("Accept")
        
        // 解析 Accept: application/vnd.api.v1+json
        version := parseVersion(accept)
        if version == "" {
            version = "v1" // 默认版本
        }
        
        c.Set("api_version", version)
        c.Next()
    }
}

func parseVersion(accept string) string {
    // 解析版本号
    if strings.Contains(accept, "vnd.api.v1") {
        return "v1"
    }
    if strings.Contains(accept, "vnd.api.v2") {
        return "v2"
    }
    return ""
}

3.2 使用自定义 Header

go
func apiVersionHandler(c *gin.Context) {
    version := c.GetHeader("API-Version")
    if version == "" {
        version = "v1"
    }
    
    switch version {
    case "v1":
        handleV1(c)
    case "v2":
        handleV2(c)
    default:
        c.JSON(400, gin.H{"error": "unsupported version"})
    }
}

// 路由注册
r.GET("/users", apiVersionHandler)

四、Query 参数版本控制

4.1 实现方式

go
func versionFromQuery() gin.HandlerFunc {
    return func(c *gin.Context) {
        version := c.Query("version")
        if version == "" {
            version = "v1"
        }
        
        c.Set("api_version", version)
        c.Next()
    }
}

func getUsers(c *gin.Context) {
    version := c.GetString("api_version")
    
    switch version {
    case "v1":
        c.JSON(200, getUsersV1Data())
    case "v2":
        c.JSON(200, getUsersV2Data())
    default:
        c.JSON(400, gin.H{"error": "invalid version"})
    }
}

// 使用: GET /api/users?version=v2

五、高级版本控制策略

5.1 版本适配器模式

go
// 定义接口
type UserService interface {
    GetUsers(ctx context.Context) ([]User, error)
    GetUser(ctx context.Context, id int) (User, error)
    CreateUser(ctx context.Context, user User) (User, error)
}

// V1 实现
type UserServiceV1 struct {
    db *sql.DB
}

func (s *UserServiceV1) GetUsers(ctx context.Context) ([]User, error) {
    // V1 实现
}

// V2 实现
type UserServiceV2 struct {
    db *sql.DB
    cache *redis.Client
}

func (s *UserServiceV2) GetUsers(ctx context.Context) ([]User, error) {
    // V2 实现,使用缓存
}

// 工厂函数
func NewUserService(version string, db *sql.DB) UserService {
    switch version {
    case "v2":
        return &UserServiceV2{db: db}
    default:
        return &UserServiceV1{db: db}
    }
}

5.2 请求转换器

go
// 请求转换器
type RequestTransformer interface {
    Transform(c *gin.Context) error
}

// V1 到 V2 的请求转换
type V1ToV2Transformer struct{}

func (t *V1ToV2Transformer) Transform(c *gin.Context) error {
    // 转换请求参数
    var body map[string]interface{}
    if err := c.ShouldBindJSON(&body); err != nil {
        return err
    }
    
    // 添加 V2 需要的字段
    body["email"] = body["email"]
    body["created_at"] = time.Now()
    
    // 重新设置 body
    c.Set("transformed_body", body)
    return nil
}

六、版本弃用策略

6.1 Sunset Header

go
func deprecationMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        version := c.GetString("api_version")
        
        if version == "v1" {
            // 添加弃用警告
            c.Header("Sunset", "Sat, 31 Dec 2025 23:59:59 GMT")
            c.Header("Deprecation", "true")
            c.Header("Link", "</api/v2/users>; rel=\"successor-version\"")
        }
        
        c.Next()
    }
}

6.2 版本统计

go
type VersionStats struct {
    Version   string
    Count     int64
    LastUsed  time.Time
}

func versionStatsMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        version := c.GetString("api_version")
        
        // 记录版本使用统计
        stats.Record(version)
        
        c.Next()
    }
}

七、最佳实践

7.1 版本控制原则

  1. 语义化版本:遵循 SemVer 规范
  2. 向后兼容:尽量避免破坏性变更
  3. 文档清晰:每个版本的差异要明确
  4. 渐进弃用:给用户足够的迁移时间

7.2 代码组织

project/
├── api/
│   ├── v1/
│   │   ├── handlers/
│   │   ├── models/
│   │   └── routes.go
│   ├── v2/
│   │   ├── handlers/
│   │   ├── models/
│   │   └── routes.go
│   └── router.go
├── internal/
└── main.go

7.3 测试策略

go
func TestAPIVersioning(t *testing.T) {
    router := setupRouter()
    
    tests := []struct {
        name       string
        version    string
        wantStatus int
    }{
        {"v1 request", "v1", 200},
        {"v2 request", "v2", 200},
        {"invalid version", "v3", 400},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            w := httptest.NewRecorder()
            req, _ := http.NewRequest("GET", "/api/users", nil)
            req.Header.Set("Accept", fmt.Sprintf("application/vnd.api.%s+json", tt.version))
            
            router.ServeHTTP(w, req)
            assert.Equal(t, tt.wantStatus, w.Code)
        })
    }
}

八、总结

策略推荐场景推荐指数
URL Path大多数场景⭐⭐⭐⭐⭐
Header内部 API⭐⭐⭐⭐
Query快速原型⭐⭐⭐
子域名大型系统⭐⭐⭐

选择合适的版本控制策略,能让你的 API 更加健壮和易维护。

上次更新于: