Go 爲什麼不支持從 main 包中導入函數?

大家好,我是煎魚。

作爲一個維護過許多有一定歷史沉澱的 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 工程項目,要交付不同種類的可組合項目的程序員來說。相當於磨滅了一條道路。還是比較尷尬的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RFsHfFByarSRAA-f1Rs13g