函数适配器应严格对齐签名、复现错误类型与全局变量,仅做协议转换而不添加逻辑,通过 import 路径伪装和完整符号导出实现无缝迁移。

直接用函数实现适配器,比结构体更轻量、无状态、易测试,但必须严格对齐签名——NewClient 返回值类型错一位、error 是指针还是值、甚至 context.Context 参数位置偏移,都会导致调用方 panic 或编译失败。
Go 函数适配器怎么写才不崩老代码
函数适配器本质是把一个旧函数“包装”成新函数签名,适用于无状态、纯协议转换场景(比如把 legacy.Do(req) 转成 modern.Do(ctx, req))。它不持有任何字段,靠闭包捕获依赖,因此天然避免了空指针、接收者类型错等问题。
- 必须显式声明所有参数和返回值,顺序、数量、类型完全一致:老接口是
func(string) (string, error),你就不能写成func(context.Context, string) (string, error)——哪怕只多一个ctx,调用方没传就会 panic - 若新版需
context.Context,而老版没有,适配器里只能用context.Background()或context.TODO()填充,不能暴露 ctx 给上游;否则老代码无法编译通过 - 返回值中
error类型必须精确匹配:老包定义的是var ErrTimeout = errors.New("timeout"),你返回fmt.Errorf("timeout")会导致errors.Is(err, legacy.ErrTimeout)判定失败 - 不要在函数适配器里做重试、超时、日志等逻辑——那是中间件或 wrapper 的事;适配器只负责“翻译”,否则职责越界,后续难替换
legacy.Do() → modern.DoWithContext() 的函数封装实操
常见迁移场景:老服务只提供 Do(url string) ([]byte, error),新 SDK 要求 Do(ctx context.Context, url string, opts ...Option) ([]byte, error)。此时不能让业务层改调用,而是用函数适配器兜底。
- 先确认老接口的
error类型是否与新 SDK 的 error 可比较:如果新 SDK 返回的是*httpError,而老接口期望error接口,那就只需原样透传;但如果老代码用errors.Is(err, legacy.ErrNetwork),你就得在适配器里手动映射错误值 - 参数转换必须显式:比如老接口传
"https://api.example.com/v1/user",新 SDK 要求拆成host+path,就得在适配器里用url.Parse()解析,不能靠 caller 传结构体 - 示例:
func Do(url string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req.WithContext(context.Background())) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body) }注意这里没暴露context,也没加timeout——因为老调用方没能力传,加了就断
import 路径不变的前提下替换 legacy 包函数
最危险的操作不是改函数,而是改 import。如果老代码 import "github.com/oldcorp/legacy",你不能指望所有人立刻改成新路径。真正的无缝,是让新函数“假装”自己就在老包里。
立即学习“go语言免费学习笔记(深入)”;
- 在项目根目录新建
legacy/legacy.go,仅导出老接口要求的函数和变量:package legacy import newclient "github.com/newcorp/sdk/v2" var ErrTimeout = newclient.ErrTimeout // 必须复现,类型和值都一样 func Do(url string) ([]byte, error) { return newclient.Do(context.Background(), url) } - 确保
legacy/legacy.go不 import 其他非必要包,尤其避免循环依赖:比如newclient又 import 了legacy,Go 会直接报错 - 用
go vet -v检查该文件是否完整实现了老包导出的所有符号;若漏了var Version = "1.2.0",老代码里fmt.Println(legacy.Version)就会 panic - 上线前跑一遍老 test:把
go test ./... -run=^TestLegacy.*,重点看是否仍能通过——这才是“无缝”的唯一验证标准
函数适配器看似简单,但最容易被忽略的是错误类型的底层实现细节和全局变量的镜像复现。一个 errors.New 和 fmt.Errorf 的差异、一个未导出字段的缺失、甚至 time.Time 字段的 JSON tag 写法,都可能让下游服务静默失败。平滑不是“看起来没报错”,而是连 errors.Is 和 json.Unmarshal 都不翻车。


















