Golang包维护者应如何正确弃用接口?
Golang包维护者应如何正确弃用接口? 假设我创建了一个供任何人使用的公共库/包。在这个库中,我导出了一个服务,如下所示。
package myLib
type LibService struct {
}
func (l LibService) GetThingName(id string) string {
... Some implementation ...
}
作为这个库的用户,我应该定义自己的接口(如此处所建议的),以减少与依赖项的耦合,便于测试时进行模拟等。
import "codeplace.com/someone/myLib"
import "codeplace.com/someone/otherpackage"
package myProject
type ThingNameGetter interface {
GetThingName(id string) string
}
func MyProjectFunction(getter ThingNameGetter) string {
return getter.GetThingName("my id")
}
现在,一段时间后,库的概念领域发生了变化,使得 GetThingName 函数不再适用。我很想进行如下更改,并让我的所有库使用者都知道该函数已弃用。
// Deprecated: Use GetNamespacedThingName
// This function will start panicking on Dec 1, 20XX
func (l LibService) GetThingName(id string) string {
... Some implementation ...
}
func (l LibService) GetNamespacedThingName(
namespace string, id string,
) string {
... Some implementation ...
}
问题是:因为 myProject 定义了自己的接口,他们不会在 IDE 中看到高亮显示的弃用警告(我认为 linter 也不会捕获它)。
因此,他们将没有时间对即将到来的函数移除做出反应。相反,它只会在 20XX 年 12 月 1 日突然“发生”,他们将不得不匆忙更新他们的使用方式。
当使用者定义自己的接口时,处理弃用的正确方法是什么?
更多关于Golang包维护者应如何正确弃用接口?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
为了确认一下。您是说,将一个函数标记为弃用(而不是移除它)也算是一种破坏性变更吗?
我有点喜欢这种做法,尽管这取决于更新库的人是否会去查找该函数的使用情况。在一个多人协作的项目中,他们可能只是执行更新,并想着“我们可以以后再处理这个问题”,然后这个函数在之后(尽管是在又一次主版本更新中)就消失了。
更多关于Golang包维护者应如何正确弃用接口?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
正如 Christoph Berger 所说:如果你的项目当前版本是 3.5.2,并且你想弃用一个函数,那么在 4.0.0 版本之前你都不能真正弃用它。如果用户依赖的是版本 3,那么他们就不会失去该功能。当他们有意识地将对你的项目的依赖升级到版本 4 时,他们才会在那时看到错误。
我不知道最佳实践是什么,但我认为在你的项目版本 3 中为函数添加弃用注释是有意义的,只要你在 v4 版本之前不实际删除它们。
是的,我在这里纠结的并不是语义化版本控制的实践问题。问题在于,依赖倒置(以及库使用者定义自己的接口)似乎被这门语言所鼓励。然而,使用这个特性和代码风格似乎使得 deprecated 标签变得价值很低。
我在网上遇到的一个解决方案是,在库中将结构体标记为废弃,并导出一个不同的结构体,该结构体排除了不再支持的旧函数。
使用这个解决方案,客户端定义的接口可以继续使用,并且客户端(在它们实例化结构体的地方)可以看到废弃提示。
唯一的问题是,这有点更繁琐,而且……嗯……不太常见。
正如Norbert所说:我认为在同一个主版本中添加弃用警告是可以的,但在下一个主版本之前,你实际上不能移除已弃用的函数。
这仍然意味着,如果使用你包的人只是将其版本依赖更新到@latest或下一个主版本,并且他们通过你提到的接口使用那段代码,那么他们的代码会突然崩溃,而他们可能对此一无所知。话虽如此,这是他们的责任。当一个包的主版本号增加时,意味着引入了破坏性的API变更。我不能依赖一个包,然后将该依赖的版本升级到该包的下一个主版本,还指望一切都能正常工作。
据我理解,这也是为什么Go语言添加泛型时,只是将次版本号从1.17增加到1.18:泛型是以向后兼容的方式添加的。如果添加泛型破坏了现有的非泛型代码,那么它就会是Go 2.0。
两点想法:
1 - “我应该定义自己的接口(如此处所建议的)以减少与依赖项的耦合”
链接后的文档指出,接口应由包的使用者创建,而不是由包的提供者创建。它似乎并没有说(如果我错了请纠正我)使用库时,使用者总是应该创建接口。
你可以在需要时创建接口,例如为了模拟测试。(并非所有测试都需要模拟库的API。) 或者当你出于解耦的原因,例如,在功能相似的包之间进行选择,并且你希望能够轻松切换时。
在具体需求出现之前就预先创建接口可能有些过度,因为YAGNI。
话虽如此,即使使用者用接口包装了一个包函数,对包的依赖也并未消失。在某个时刻,接口必须被实现,而如果该实现使用了已弃用的包函数,弃用信息就不应被忽视。
2 - 弃用与破坏客户端代码
当一个函数从包API中移除时,这是一个破坏性变更,需要更新主版本号,以符合语义化版本控制原则和Go模块的版本控制规则。
如果客户端想要使用模块的更高主版本,他们必须有意识地更新其依赖项(并相应地更改导入路径)。如果他们不这样做,他们将继续使用仍包含已弃用函数的旧版本,对他们来说一切照旧。
这就引出了一个有趣的问题——模块的语义化版本控制规则是否使得函数级别的//Deprecated:注释变得过时?如果破坏性变更无论如何都需要新的主版本,那么函数级别的弃用信息似乎就没多大必要了。
当库维护者需要弃用接口方法时,用户自定义接口确实会导致弃用警告无法传递。以下是几种专业处理方案:
1. 提供官方接口(推荐)
在库中同时提供具体类型和接口定义:
package myLib
// LibService 具体类型
type LibService struct{}
// Service 官方接口
type Service interface {
GetThingName(id string) string
GetNamespacedThingName(namespace, id string) string
}
// 确保 LibService 实现 Service 接口
var _ Service = (*LibService)(nil)
// Deprecated: Use GetNamespacedThingName
// This function will be removed in v2.0.0
func (l LibService) GetThingName(id string) string {
// 实现
}
func (l LibService) GetNamespacedThingName(namespace, id string) string {
// 实现
}
2. 使用编译时检查
在库中添加接口兼容性检查:
package myLib
// DeprecatedInterface 标记已弃用的接口方法集合
type DeprecatedInterface interface {
GetThingName(id string) string
}
// 在测试或初始化时检查
func init() {
var s LibService
var _ DeprecatedInterface = s // 编译时检查
}
// Deprecated: Use GetNamespacedThingName
func (l LibService) GetThingName(id string) string {
// 实现
}
3. 运行时警告
在已弃用的方法中添加日志输出:
package myLib
import (
"log"
"runtime"
)
// Deprecated: Use GetNamespacedThingName
func (l LibService) GetThingName(id string) string {
// 输出调用栈信息
pc := make([]uintptr, 10)
n := runtime.Callers(2, pc)
frames := runtime.CallersFrames(pc[:n])
log.Printf("WARNING: GetThingName is deprecated and will be removed. Called from:")
for {
frame, more := frames.Next()
log.Printf(" %s:%d %s", frame.File, frame.Line, frame.Function)
if !more {
break
}
}
// 原有实现
return l.getThingNameImpl(id)
}
4. 版本化弃用策略
使用语义化版本和文档明确弃用时间线:
package myLib
import "time"
// v1.2.0: 添加弃用警告
// v1.3.0: 函数返回错误
// v2.0.0: 移除函数
var deprecationDeadline = time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC)
// Deprecated: Use GetNamespacedThingName
// This function will return an error after Dec 1, 2024
func (l LibService) GetThingName(id string) (string, error) {
if time.Now().After(deprecationDeadline) {
return "", errors.New("GetThingName is deprecated, use GetNamespacedThingName")
}
return l.getThingNameImpl(id), nil
}
5. 提供迁移辅助工具
创建检测已弃用用法的工具:
// tools/deprecation_check/main.go
package main
import (
"go/ast"
"go/parser"
"go/token"
"log"
)
func checkDeprecatedUsage(filename string) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
ast.Inspect(node, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.CallExpr:
if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
if sel.Sel.Name == "GetThingName" {
log.Printf("Deprecated usage found at %s:%d",
filename, fset.Position(sel.Pos()).Line)
}
}
}
return true
})
}
6. 接口适配器模式
提供向后兼容的适配器:
package myLib
// NamespacedService 新接口
type NamespacedService interface {
GetNamespacedThingName(namespace, id string) string
}
// DeprecatedAdapter 适配旧接口
type DeprecatedAdapter struct {
Service NamespacedService
DefaultNamespace string
}
func (a DeprecatedAdapter) GetThingName(id string) string {
return a.Service.GetNamespacedThingName(a.DefaultNamespace, id)
}
实际示例
综合应用上述策略:
package myLib
import (
"fmt"
"time"
)
// Service 官方接口定义
type Service interface {
// Deprecated: Use GetNamespacedThingName
// Will be removed in v2.0.0
GetThingName(id string) string
GetNamespacedThingName(namespace, id string) string
}
type LibService struct {
defaultNamespace string
}
var _ Service = (*LibService)(nil)
func (l LibService) GetThingName(id string) string {
// 输出弃用警告
if os.Getenv("MYLIB_STRICT") == "1" {
panic("GetThingName is deprecated")
}
fmt.Fprintf(os.Stderr,
"WARNING: GetThingName is deprecated, use GetNamespacedThingName\n")
return l.GetNamespacedThingName(l.defaultNamespace, id)
}
func (l LibService) GetNamespacedThingName(namespace, id string) string {
// 新实现
return fmt.Sprintf("%s/%s", namespace, id)
}
// 提供迁移函数
func MigrateToNamespaced(getter interface {
GetThingName(id string) string
}, namespace string) Service {
return &migratedService{
oldGetter: getter,
namespace: namespace,
}
}
type migratedService struct {
oldGetter interface {
GetThingName(id string) string
}
namespace string
}
func (m *migratedService) GetThingName(id string) string {
return m.oldGetter.GetThingName(id)
}
func (m *migratedService) GetNamespacedThingName(namespace, id string) string {
// 模拟旧行为
return m.oldGetter.GetThingName(id)
}
关键点:
- 在库中提供官方接口定义
- 使用编译时和运行时检查
- 明确的版本弃用策略
- 提供迁移路径和工具
- 通过环境变量控制严格模式
这样即使使用者定义了自定义接口,也能通过多种渠道获知弃用信息,并有充足时间进行迁移。


