适配器函数必须保持输入输出类型一致,接收userv1返回userv1(或指针),内部做字段赋值与默认值填充,禁止panic、i/o及手动映射,嵌套结构需递归适配,确保双向兼容性。

旧版数据结构直接替换会引发 panic 或字段丢失,必须用适配器函数做单向转换,而不是靠调用方手动映射。
适配器函数必须严格保持输入输出类型一致
新版服务期望接收 UserV2 结构体,但老系统只提供 UserV1;这时不能写一个返回 *UserV2 的函数然后让所有调用点改用它——因为下游可能还在用 UserV1 的字段做判断或日志。真正安全的做法是:适配器函数签名必须和原调用点完全一致,即接收 UserV1,返回 UserV1(或其指针),内部仅做字段赋值与兼容性兜底。
- 错误示例:
func ToUserV2(u UserV1) *UserV2—— 调用方需显式改写,漏一处就崩 - 正确做法:
func AdaptUserV1(u UserV1) UserV1,在内部把废弃字段补默认值、重命名字段拷贝过去、空字段设零值 - 若原接口接收
*UserV1,适配器也必须接收*UserV1并原地修改,避免返回新地址导致指针比较失效(如u == nil判断)
字段缺失时不能 panic,要按语义填充默认值
老结构体没有 CreatedAt 字段,而新逻辑依赖它做 TTL 判断——此时不能留空或传零时间,否则后续业务可能误判为“未来时间”。适配器函数里必须根据上下文注入合理默认值。
-
CreatedAt缺失 → 填time.Now().UTC()(适用于新建导入场景) -
Status从 string 改为 enum 类型 → 按字符串值映射:if u.Status == "active" { u.Status = StatusActive } -
Tags从[]string改为map[string]string→ 若老数据只有 tag 名,value 统一填"1",保证 map 不为 nil - 绝不使用
panic("missing field")或log.Fatal—— 适配器必须静默容错
嵌套结构变更要用递归适配,别只 flatten 一层
当 UserV1.Profile 是一个扁平结构,而 UserV2.Profile 变成嵌套的 ProfileV2{Contact: ContactV2, Address: AddressV2},只做顶层字段复制会丢掉深层兼容性。
- 必须拆出独立的
AdaptProfileV1ToV2函数,并在主适配器中调用 - 若
UserV1.Profile本身是nil,UserV2.Profile也不能是nil,应初始化空结构体(防止后续 dereference panic) - 避免在适配器里做 I/O 或网络调用 —— 它只负责内存结构转换,超时、重试、fallback 都该由上层控制
- 用
go vet -v检查是否所有字段都被覆盖(尤其新增字段),未显式赋值的 struct 字段会保留零值,可能触发隐式 bug
最易被忽略的是字段生命周期:老结构体中某个字段在 v1 是必填,在 v2 变成可选,但反向迁移时,v2 数据写回 v1 存储前,仍需确保该字段不为空——适配器不是单向翻译器,而是双向契约守门员。











