如何在库中安全维护 Wire Provider Set:哪些修改不破坏现有 injector?
【免费下载链接】wireCompile-time Dependency Injection for Go项目地址: https://gitcode.com/GitHub_Trending/wi/wire
当你的 Go 库通过wire.NewSet暴露 provider set,供其他应用写进wire.Build时,每次改动都可能影响消费方已生成的wire_gen.go。本文基于 Wire 项目文档中的最佳实践,给出判断哪些修改与现有 injector 兼容、哪些会破坏兼容的标准,并说明如何在发布前用wire命令实际验证改动对现有 injector 的影响。适用前提:消费方项目已按 Wire 的方式定义了 injector(函数体只含wire.Build调用),并已在源码管理中提交了wire.go与wire_gen.go。
先明确:provider set 的输入/输出类型就是兼容边界
Wire 的核心概念是 provider(能产出某个类型的函数)和 injector(按依赖顺序调用 provider 的函数)。库作者用wire.NewSet把一组 provider 打包成 provider set,消费方在 injector 声明中引用它,Wire 在代码生成阶段为 injector 填入实现,输出到wire_gen.go(见 docs/guide.md)。
对库的维护者来说,有两点决定了"什么改动是安全的":
- Wire 通过类型标识匹配输入和输出,provider set 暴露的输入类型集合与输出类型集合,就是它对消费方的契约;
- 一个重要事实:把 provider set 里某个输出对应的 provider 换成另一个函数后,消费方现有的 injector 在重新生成之前仍在使用旧的 provider(docs/best-practices.md 明确指出 "existing injectors will use the old provider until they are regenerated")。也就是说,
wire_gen.go不会随库升级自动更新,改动是否"破坏"要分两层看:能否让消费方重新生成成功,以及不重新生成时行为是否可接受。
哪些修改是安全的
docs/best-practices.md 给出了明确的判断标准。在不破坏兼容性的前提下,库里的 provider set 只允许两类修改:
1. 更换某个已有输出的 provider,但不引入新的输入
条件:
- 新 provider 产出的类型与原来相同;
- 新 provider不得引入新的输入类型(参数类型集合不能变大);
- 允许减少输入(移除输入是安全的);
- 注意上面提到的限制:消费方在重新生成 injector 之前仍走旧 provider。
2. 向 provider set 中引入一个全新的输出类型
条件:
- 该输出类型必须是新引入的类型,而不是某个已经存在的类型;
- 文档要求该类型与提供它的 provider 在同一个 commit/发布中一起引入,避免消费方拿到"provider 已发布、类型尚未发布"的中间状态。
判断"是否已有输出类型"时,冲突的真实形态是 Wire 的multiple bindings错误。项目测试数据(internal/wire/testdata/MultipleBindings/want/wire_errs.txt,示例结果,其中的x:y是占位位置)展示了这类错误的样子:
example.com/foo/wire.go:x:y: multiple bindings for example.com/foo.Foo current: <- provider "provideFooAgain" (example.com/foo/foo.go:x:y) previous: <- provider "provideFoo" (example.com/foo/foo.go:x:y)只要消费方的 provider set 里已存在同一类型的 provider(无论来自另一个 provider set、wire.Value还是wire.Bind),再给这个类型加 provider 就会触发该错误。这也是下一条"不安全修改"会破坏 injector 的原因。
哪些修改会破坏现有 injector
除上述两类外,其余修改都不安全(docs/best-practices.md):
- 要求新的输入:provider set 新增一个只有消费方才需要提供值的类型,消费方的 injector 必须重新生成并补齐该输入,否则生成失败;
- 移除某个输出类型:依赖该输出的消费方 injector 会直接坏掉;
- 把一个已存在的输出类型加进 provider set:可能和消费方已有的 provider 冲突,产生
multiple bindings错误。
文档给出的替代方案是:新增一个 provider set,而不是修改现有 set。
下面这段代码是 docs/best-practices.md 中的官方示例,完整展示了"可以做什么、不可以做什么"(Greeter、Message等为示例类型):
var GreeterSet = wire.NewSet(NewStdoutGreeter) func DefaultGreeter(ctx context.Context) *Greeter { // ... } func NewStdoutGreeter(ctx context.Context, msgs []Message) *Greeter { // ... } func NewGreeter(ctx context.Context, w io.Writer, msgs []Message) (*Greeter, error) { // ... }对照这个 set:
- 允许:在
GreeterSet中用DefaultGreeter替换NewStdoutGreeter——两者都不引入新输入; - 允许:新建一个类型
T并为它添加 provider 放入GreeterSet,只要T与 provider 在同一个 commit/发布中引入; - 不允许:在
GreeterSet中用NewGreeter替换NewStdoutGreeter——它新增了输入类型io.Writer,并且使*Greeter的 provider 开始返回error,消费方的 injector 签名需要相应变化; - 不允许:从
GreeterSet中移除NewStdoutGreeter——依赖*Greeter的消费方 injector 会坏掉; - 不允许:给
GreeterSet添加io.Writer的 provider——消费方可能已经有io.Writer的 provider,会冲突。
如何验证一次修改不会破坏现有 injector
验证动作发生在消费方的项目里(库代码修改后,在引用了该 provider set 的 injector 所在包中操作)。wire命令行工具提供了几个子命令,其行为以 cmd/wire/main.go 中的 usage 说明为准,均支持[packages]参数,未指定包时默认.。
先确认工具可用。按 README.md 安装,并确保$GOPATH/bin在$PATH中:
go install github.com/google/wire/cmd/wire@latest第一步:用wire diff预览对现有 wire_gen.go 的影响
diff子命令"generates the content for their wire_gen.go files and outputs the diff against the existing files",即生成将写入的内容并与现有文件对比输出,不修改文件,适合在改动后先做检查。按 usage 说明,它"returns 0 if no diff, 1 if different, 2 plus an error if trouble":
wire diff ./...- 退出码 0 且无输出:库修改后重新生成结果与原
wire_gen.go一致,现有 injector 不受影响; - 退出码 1 并输出 unified diff:生成结果有变化,消费方需要重新生成
wire_gen.go。此时对照 diff 内容核对变化是否属于前文"安全修改"的预期(例如只是 provider 函数被替换,而按文档说明旧 injector 在重新生成前仍使用旧 provider); - 退出码 2:加载或生成出错,按输出的错误信息排查。
第二步:用wire check检查类型与 Wire 错误
check子命令"prints any type-checking or Wire errors found with top-level variable provider sets or injector functions":
wire check ./...如果消费方的wire.Build与新版本的 provider set 组合后存在multiple bindings、缺少 provider 等问题,会在这里以错误形式打印出来(教程中展示了缺 provider 时的错误示例,见 _tutorial/README.md)。没有输出错误即表示该包内 provider set 与 injector 声明通过了 Wire 的检查。
第三步:确认可接受后,在消费方重新生成
重新生成有两条等价路径(docs/guide.md):
# 在包含 injector 的包目录内直接运行 wire # 或者依赖生成文件中的指令 go generate生成产物写入wire_gen.go(如 wire 生成的示例文件 所示,文件头带有// Code generated by Wire. DO NOT EDIT.)。wire_gen.go头部包含//go:generate go run -mod=mod github.com/google/wire/cmd/wire,因此go generate可以持续再生成它。
如需了解某个包中顶层 provider set 导入了哪些 set、在各输入条件下能产出哪些输出类型,可以用wire show [packages]查看(cmd/wire/main.go 中的 usage:列出 provider set 的 imports、"Outputs given " 以及包内定义的 injector 函数),适合在改动前记录 set 的输入/输出基线,与改动后的输出对照,判断是否落入"要求新的输入 / 移除输出 / 加入已有输出"这三类破坏性修改。
设计库 provider set 时的配套建议
best-practices 文档同时给出了从源头降低破坏概率的设计建议:
- 保持库的 provider set 小而窄:文档建议库 set 通常只包含单个 provider 函数,外加一条
wire.Bind把返回类型绑定到它实现的接口。docs/guide.md 补充了配套约束:"Any set that includes an interface binding must also have a provider in the same set that provides the concrete type"——含wire.Bind的 set 必须在同一 set 内提供对应具体类型的 provider。 - 不要把通用类型打包进库 set:文档以 web 服务客户端为例说明,虽然很有吸引力,但不要在自己的客户端 provider set 里捆绑
*http.Client的 provider,因为每个库都这么做时彼此会冲突;正确做法是 set 只包含 API 客户端的 provider,把*http.Client留作 set 的输入,由消费方提供。 - 需要多个同类型依赖时定义新类型:如 docs/faq.md 所示,同一类型只能有一个 provider,合法的同类型多依赖场景应发明新类型(如
type OtherFoo Foo)再包装/解包,避免触发multiple bindings。
限制与说明
- 兼容规则只覆盖"换 provider 且不新增输入""新增全新输出类型"两类修改;换 provider 时消费方的旧
wire_gen.go在重新生成前不会自动切换到新 provider,行为差异由库作者自己评估。 - 按 README.md 的项目状态说明,Wire 自 v0.3.0 起为 beta 且功能完整,项目方表示不再接受新特性,仓库已声明不再维护(扩展需在 fork 中进行)。本文的维护规则以仓库当前文档为准。
wire diff、wire check、wire show的子命令行为取自 cmd/wire/main.go 中注册的命令 usage 文本,可用wire help或wire commands在本地查看完整列表。
【免费下载链接】wireCompile-time Dependency Injection for Go项目地址: https://gitcode.com/GitHub_Trending/wi/wire
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考