必须为不同API版本显式绑定独立数据库实例、表结构和Go结构体:v1用user_v1表与UserV1结构体,v2用user_v2表与UserV2结构体,禁止复用DB实例或struct类型,路由与DB严格绑定,迁移需独立脚本且字段变更须先于接口上线。

不能靠“同一个数据库模型适配多个 API 版本”来实现,必须为不同版本 API 显式绑定各自对应的数据库实例和结构体。Golang 没有运行时模型切换机制,所谓“v1 用 user_v1 表、v2 用 user_v2 表”,本质是两个物理表 + 两个独立 *gorm.DB 实例 + 两套 struct 定义。
每个 API 版本对应一个独立的 *gorm.DB 实例
混用 *gorm.DB 是最常见也是最危险的错误:v2 handler 误调用 v1 的 DB 实例,会查到旧表、写错 schema、事务失效。
- 别用全局
var db *gorm.DB,改用命名明确的变量,如v1UserDB、v2UserDB - DSN 必须完整指向不同库或不同 schema:
user:pass@tcp(10.0.1.10:3306)/api_v1?...和user:pass@tcp(10.0.1.10:3306)/api_v2?... - 初始化失败必须显式处理,不能放在
func init()里——错误无法返回,服务启动后才暴露 - 确认已导入驱动:
import _ "gorm.io/driver/mysql",漏掉下划线会导致首次查询卡死或报"invalid connection"
v1/v2 的 struct 必须物理隔离,不可共用
哪怕字段只差一个 json:"avatar_url,omitempty",v1 和 v2 的用户结构体也该是两个独立类型。共用 struct + 条件编译或 tag 切换,会在 JSON 序列化、GORM 映射、字段校验层面埋下兼容性雷。
- v1 版本用
type UserV1 struct { ID uint `gorm:"primaryKey"` Name string } - v2 版本用
type UserV2 struct { ID uint `gorm:"primaryKey"` Name string AvatarURL string `json:"avatar_url,omitempty"` } - 不要在 v1 struct 里加
// deprecated字段试图“兼容”v2——这会让 v1 的序列化输出意外多出字段,破坏契约 - GORM 的
TableName()方法可按版本返回不同表名,但前提是 struct 类型本身不复用,否则db.Table("user_v2").Create(&u)仍可能因字段映射冲突失败
路由分组与 DB 实例必须严格绑定
API 路由不是装饰,而是执行上下文的边界。/v1/users 的 handler 只能访问 v1UserDB 和 UserV1,任何跨版本引用都应被编译器或 linter 拦住。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 用
chi或gin.Group("/v1")创建子路由,handler 函数签名中直接注入对应 DB 实例:func getUsersV1(db *gorm.DB) gin.HandlerFunc - 避免在中间件里把 DB 实例塞进
context.Context再取出来——容易传错、难追踪、测试不便 - 如果用依赖注入(如 wire),给每个 DB 绑定唯一类型别名:
type V1UserDB *gorm.DB和type V2UserDB *gorm.DB,防止容器注入混淆 - 单元测试时,v1 handler 必须用
v1UserDB的 mock 或 testcontainer,绝不能复用 v2 的测试 DB 连接
跨版本数据迁移不是 ORM 责任,得靠脚本+业务层补偿
上线 v2 后,老数据要从 user_v1 迁到 user_v2,这不是 GORM AutoMigrate 能解决的。字段语义变化(如 name → full_name + first_name)、新增非空约束、索引变更,都需定制逻辑。
- 迁移脚本应独立于主应用二进制,用
spf13/cobra做命令行工具,通过 flag 指定源/目标 DB 实例 - 迁移过程必须可中断、可重入:记录已处理主键范围,失败后从断点继续,不重复写
- 上线 v2 后,v1 接口仍需维持读能力(只读),但禁止写;v2 接口写新表,同时异步双写或发消息触发 v1 表补全(如日志库)
- 别指望 “用同一个 model + 不同 Table() 实现平滑过渡”——GORM 不支持运行时动态切换字段映射规则,字段增删改必然导致
Scan失败或静默丢数据
最易被忽略的一点:struct 字段变更和数据库表结构变更必须同步推进,且顺序不能反。先加字段(DB 迁移 + struct 扩展),再上线新接口;而不是先改 handler 返回新字段,再补 DB 和 struct —— 这会导致 GORM 查询时 panic 或返回零值。

















