Golang: Codebase Refactoring

 9th September 2022 at 4:06pm

这里描述重构一个 Golang codebase 的场景。

会写这篇文章是看了 Russ Cox 写的 Codebase Refactoring 文章。里面描述了几个需要重构代码库的例子,比如将一开始的 os.Error 挪至 builtin.Error,以及 Kubernetes 和 Docker 需要挪动 utils 类到别的包的场景。Russ 的文章写得非常细致了,但是读起来比较花时间,这里做个简要的总结

很多 Go 项目一开始都是以单个包的形式开发的。随着功能演进,慢慢有了将公开 API 挪动到别的包(比如子目录中)的需求。期望的效果是,包的用户在升级到包的新版本时,不需要马上改代码,而是可以渐进地把代码改到新 API:

Go 代码中,作为公开的符号 可以被访问的有:

  • 常量
  • 函数
  • 全局变量
  • 类型

对旧包的这几种符号做改造的方法有:

const OldAPI = NewPackage.API

func OldAPI() { NewPackage.API() }

var OldAPI = NewPackage.API

type OldAPI ... ???

对于类型(type OldAPI),我们放到后面再讲。

除了类型外的几种符号,使用旧包的和使用新包的,在效果上是非常等同的,除了一些细微的差别:

  • 常量:完全没区别,编译期就确定了。
  • 函数:有少量区别,比如使用旧 API 会比新 API 多一层栈帧,其他没啥区别。
  • 全局变量:有非常少的区别。全局变量一般会在程序启动时被初始化,之后不再允许被修改(避免 data races)。即使新旧全局变量的内存地址不一样,也很少场景需要使用其地址。

对于类型(type),你也许会说用类型定义:

type OldAPI NewAPI

但是事实上它们是两个不同的类型。对于这样一个函数:

func Fn(o *OldAPI) {}

它是不接受一个 *NewAPI 对象作为参数传入的。

事实上,你应该使用 type alias:

type OldAPI = NewAPI

此时 OldAPINewAPI 就是两个一样的类型。

类型的判定规则描述在 这里,我没有细看。

参考

关于 type alias 这个特性的设计过程,这两篇 GitHub issue 有巨量的讨论,感觉非常有价值。但我还没有精力和能力去消化它们: