应统一核心User结构体,通过嵌入、匿名字段和自定义JSON marshal控制版本输出;版本差异应落在序列化行为与字段赋值时机,而非重复定义struct。

用类型组合替代重复 struct 定义
版本多了最直观的冗余就是 UserV1、UserV2、UserV3 这类几乎一模一样的结构体。硬拆会导致字段增减、JSON tag 变更、校验逻辑散落各处,改一个漏一个。
真正可维护的做法是:只保留一个核心 User 结构体,按需通过嵌入 + 匿名字段 + 自定义 JSON marshal 控制输出。比如 v2 新增 avatar_url 字段,不要另建 UserV2,而是:
- 在
User中添加AvatarURL string `json:"avatar_url,omitempty"` - v1 handler 里显式清空该字段:
u.AvatarURL = "",或用中间件统一过滤 - v2 handler 正常赋值,由 JSON 序列化自动控制是否输出
这样既复用数据模型,又避免了结构体爆炸。关键点在于:版本差异应落在「序列化行为」和「字段赋值时机」,而非结构体定义本身。
把版本逻辑下沉到 service 层参数控制
常见错误是每个版本 handler 都重写一遍 DB 查询、校验、组装逻辑,比如 getUsersV1 和 getUsersV2 各自调一次 db.FindUsers() 再手动拼字段。
立即学习“go语言免费学习笔记(深入)”;
应该让 service 方法接受版本参数,由它决定要不要查关联表、要不要加载额外字段、要不要做兼容性转换:
-
service.GetUser(ctx, id, "v1")返回基础字段 -
service.GetUser(ctx, id, "v2")多查一张 profile 表,合并返回 - handler 层只剩路由分发和响应包装,不再承担业务判断
这样新增 v3 时,只需扩展 service 的 switch 分支,不用动任何 handler 或 model。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
用中间件统一注入版本上下文,避免 handler 里反复解析
如果坚持用 URL 路径版本(如 /v2/users),别在每个 handler 里写 strings.HasPrefix(r.URL.Path, "/v2/") —— 这种硬编码会漏判、难测试、无法复用。
正确做法是在路由注册后、handler 执行前,用中间件提取并注入版本信息到 context.Context:
- 用 chi 或 Gin 的
Use注册中间件,从r.URL.Path提取v1或v2 - 存入 ctx:
ctx = context.WithValue(ctx, versionKey, "v2") - 后续所有 handler 通过
ctx.Value(versionKey)获取,不依赖路径字符串匹配
好处是:版本感知与路由解耦;测试时可直接传入 mock ctx;未来切 Header 方案也只需改中间件,handler 零改动。
按版本组织 handler 包,但共用底层 service 和 model
目录结构容易踩坑:有人建 handlers/v1/ 和 handlers/v2/,结果两个包各自 copy-paste 了 service 调用和 error 处理逻辑。
推荐结构是:
-
internal/service/—— 所有业务逻辑,带版本参数 -
internal/model/—— 统一数据结构,含版本敏感字段 -
handlers/v1/和handlers/v2/—— 仅包含路由绑定、请求解析、响应包装
这样 v1/v2 的差异被压缩到最小范围,新增版本只需新建 handler 包,不碰 service 和 model。最难的是保持 service 接口的扩展性——别让它一开始就被 v1 需求锁死,预留 opts ...Option 参数比加一堆 bool 标志更可持续。

















