大家好,我是煎鱼。
作为一个维护过许多有一定历史沉淀的 Go 项目的人,在历史债务下和奇葩需求下,会遇到一些迫于业务需求的技术诉求。
诉求上是希望引用多项目,会出现从 main 包(package)中导入相关函数的这种使用诉求。为了将多 Go 工程合并到一个大单体中使用。
问题案例
具体的使用案例如下。
我们有一个 Go 应用,目录结构如下:
demo1
├── go.mod
├── main.go
└── x
└── main.go
demo1/x/main.go 文件内代码如下:
package main
import (
"fmt"
)
func main() {
Main()
}
func Main() {
fmt.Println("煎鱼进水了?")
}
demo1/main.go 文件内代码如下:
package main
import (
"fmt"
xmain "example.com/greet/x" // 也就是本应用,上面的 x
)
func main() {
fmt.Println("脑子进煎鱼了!")
xmain.Main()
}
简单来讲,就是 demo1 这个 Go 项目,拥有两个 main 包。根目录下的 main.go 文件内引用了 x/main.go 内的 Main 方法。
运行该程序,看看运行结果:
$ go run main.go
main.go:6:2: import "example.com/greet/x" is a program, not an importable package
会直接报错,提示 x 包下是一个程序,而不是一个可导入的包。
为什么不支持导入 main 包
这个问题稍微可以收敛一下,关键内容是:为什么不支持导入 main 包内的函数?明明 main 包也是一个 package,其个别函数也是大写开头,是允许对外导出的。
我首先翻阅了一下 Go 语言规范(spec),确实没有非常明确禁止该项行为。但又确实在我们日常使用和编译运行时,会被拒绝运行。提示前面的错误。
随后又查看了具体的代码提交和 CL,实际上在 13 年前。现任 Go 核心团度负责人 @rsc 是提交过相应 main 包支持的。
如下 CL 所示:
图片
2011 年(13 年前)的 CL 移除了原本语言规范中定义的 “程序中的其他包都不能命名为 main” 的要求,也就是可以满足前文问题和背景中提到的使用诉求。
看到这里有的同学就疑惑了。怎么 13 年后的现在,2024 年。又不行了呢?而且感觉是不行好久了。
因为在 2015 年时,现任 Go 核心团队成员 @ianlancetaylor,又又又改了,增加了非常明确的判断,直接限制了。
如下代码变更:
图片
比较有趣的是,@rsc 和 @ianlancetaylor 的变更都是针对同一个 issues #4210:《cmd/go: go build does not reject importing commands》。
怎么后面又变了呢?@ianlancetaylor 给出的明确答复和定义:
图片
CL 4126053(原先 @rsc 提交的那次)是对描述语言规范的修改。该语言允许导入名为 main 的包。例如:在为使用 main 包的命令中的函数编写单元测试时,就可以使用它。
但这里的问题是关于 Go 工具,而不是语言。问题是 go 工具是否应该允许软件包导入定义命令的包。普遍的共识是不应该。
所提及的 Go 工具,覆盖的范围是:cmd/go。包含了 go build 等相关命令。因此是在受限制范围的。
经过如此切分场景,就能知道为什么语言规范上没有明确禁止。但 Go 工具上又明确拒绝了。因为其对应覆盖了不同的使用场景。
不支持的原因,结合讨论来看。
普遍认为支持 main 包的导入,会造成更大的复杂度和不安全性。
像是在 main 函数在编写时,通常会假定自己拥有完全的控制权,因此多个 main 包内的函数引入,可能会造成在 init 函数的初始化顺序、全局变量的注册等,都会产生程序上的冲突。
总结
在本次对 Go 工具限制从 main 包中导入相关函数的缘由,我们做了详尽的了解和分析。虽然 Go 官方这样的方式可以一刀切的解决复杂度和安全性的问题。
但有历史沉淀、债务的情况下,对于需要维护多个 Go 工程项目,要交付不同种类的可组合项目的程序员来说。相当于磨灭了一条道路。还是比较尴尬的。