關於 go 命令

Go 發行版包含一個名為“go”的命令,它可以自動下載、構建、安裝和測試 Go 包和命令。本文件將討論我們為何編寫這個新命令、它是什麼、它不是什麼,以及如何使用它。

動機

您可能看過早期 Go 的演講,其中 Rob Pike 曾開玩笑說,Go 的想法是在等待一個大型 Google 伺服器編譯時產生的。這確實是 Go 的動機:構建一種語言,能夠很好地用於構建 Google 編寫和執行的大型軟體。從一開始就很清楚,這樣的語言必須提供一種清晰表達程式碼庫之間依賴關係的方法,因此出現了包分組和顯式的匯入塊。從一開始也很清楚,您可能希望用任意語法來描述正在匯入的程式碼;這就是為什麼匯入路徑是字串字面量的原因。

Go 從一開始的一個明確目標就是,僅憑原始碼本身的資訊就能構建 Go 程式碼,而無需編寫 makefile 或其許多現代替代品。如果 Go 需要一個配置檔案來解釋如何構建您的程式,那麼 Go 就失敗了。

起初,還沒有 Go 編譯器,最初的開發專注於構建一個編譯器,然後為其構建庫。為了方便起見,我們推遲了使用 make 和編寫 makefile 來自動化構建 Go 程式碼。當編譯單個包涉及多次呼叫 Go 編譯器時,我們甚至使用了一個程式為我們編寫 makefile。如果您深入挖掘儲存庫歷史,就可以找到它。

新 go 命令的目的是讓我們迴歸這個理想,即 Go 程式應該無需任何配置或開發者額外的努力(除了編寫必要的匯入語句)即可編譯。

配置與約定

實現無配置系統的簡單性的方法是建立約定。該系統僅在其遵循這些約定的範圍內執行。當我們首次推出 Go 時,許多人釋出的包為了被使用,必須安裝在特定的位置、使用特定的名稱、藉助特定的構建工具。這是可以理解的:在大多數其他語言中都是這樣工作的。在過去的幾年裡,我們一直提醒人們注意 goinstall 命令(現在已被 go get 取代)及其約定:首先,匯入路徑以一種已知的方式從原始碼的 URL 派生;其次,在本地檔案系統中儲存原始碼的位置以一種已知的方式從匯入路徑派生;第三,原始碼樹中的每個目錄都對應一個包;第四,該包僅使用原始碼中的資訊進行構建。如今,絕大多數包都遵循這些約定。Go 生態系統因此變得更簡單、更強大。

我們收到了許多要求,允許在包目錄中提供一個 makefile,以提供比原始碼中多一點額外的配置。但這會引入新的規則。由於我們沒有滿足這些要求,因此我們能夠編寫 go 命令並消除對 make 或任何其他構建系統的使用。

重要的是要理解,go 命令不是一個通用的構建工具。它無法配置,並且除了 Go 包之外,它不嘗試構建任何東西。這些是重要的簡化假設:它們不僅簡化了實現,更重要的是,簡化了工具本身的使用。

Go 的約定

go 命令要求程式碼遵循幾個關鍵的、成熟的約定。

第一,匯入路徑以一種已知的方式從原始碼的 URL 派生。對於 Bitbucket、GitHub、Google Code 和 Launchpad,儲存庫的根目錄透過儲存庫的主 URL 識別,不包含 https:// 字首。子目錄是透過在該路徑上新增來命名的。例如,Google 日誌包 glog 的原始碼透過執行以下命令獲得:

git clone https://github.com/golang/glog
因此,glog 包的匯入路徑是“github.com/golang/glog”。

這些路徑比較長,但作為回報,我們獲得了匯入路徑的自動管理名稱空間,以及像 go 命令這樣的工具能夠檢視不熟悉的匯入路徑並推斷出原始碼的獲取位置的能力。

第二,在本地檔案系統中儲存原始碼的位置以一種已知的方式從匯入路徑派生,具體為 $GOPATH/src/<import-path>。如果未設定,$GOPATH 預設為使用者主目錄中名為 go 的子目錄。如果 $GOPATH 被設定為路徑列表,go 命令會嘗試搜尋該列表中的每個目錄下的 <dir>/src/<import-path>

按照約定,這些樹中的每個目錄都包含一個名為“bin”的頂級目錄,用於存放編譯好的可執行檔案,一個名為“pkg”的頂級目錄,用於存放可被匯入的編譯好的包,以及“src”目錄,用於存放包的原始檔。強制執行此結構使我們能夠保持每個目錄樹的獨立性:編譯形式和原始碼始終在一起。

這些命名約定還使我們能夠反向工作,從目錄名到其匯入路徑。這個對映對於 go 命令的許多子命令很重要,如下文所示。

第三,原始碼樹中的每個目錄對應一個包。透過將目錄限制為一個包,我們不必建立混合匯入路徑,即先指定目錄,然後指定該目錄內的包。此外,大多數檔案管理工具和使用者介面都以目錄為基本單位進行操作。將 Go 的基本單元——包——與檔案系統結構繫結,意味著檔案系統工具就成了 Go 包工具。複製、移動或刪除一個包相當於複製、移動或刪除一個目錄。

第四,每個包僅使用原始檔中存在的資訊進行構建。這使得工具更能適應不斷變化的構建環境和條件。例如,如果我們允許額外的配置,如編譯器標誌或命令列配方,那麼每次構建工具更改時都需要更新該配置;它還將與特定工具鏈的使用緊密繫結。

開始使用 go 命令

最後,快速瀏覽一下如何使用 go 命令。如上所述,Unix 上的預設 $GOPATH$HOME/go。我們將在此儲存程式。要使用其他位置,您可以設定 $GOPATH;有關詳細資訊,請參閱 如何編寫 Go 程式碼

我們首先新增一些原始碼。假設我們要使用 codesearch 專案中的索引庫以及一個左傾紅黑樹。我們可以使用“go get”子命令安裝兩者:

$ go get github.com/google/codesearch/index
$ go get github.com/petar/GoLLRB/llrb
$

這兩個專案現在都已下載並安裝到 $HOME/go 中,其中包含 src/github.com/google/codesearch/index/src/github.com/petar/GoLLRB/llrb/ 這兩個目錄,以及這些庫及其依賴項的編譯好的包(在 pkg/ 中)。

因為我們使用版本控制系統(Mercurial 和 Git)來簽出原始碼,所以原始碼樹還包含相應儲存庫中的其他檔案,例如相關包。“go list”子命令列出與其引數對應的匯入路徑,而模式“./...”表示從當前目錄(“./”)開始,查詢該目錄下的所有包(“...”)

$ cd $HOME/go/src
$ go list ./...
github.com/google/codesearch/cmd/cgrep
github.com/google/codesearch/cmd/cindex
github.com/google/codesearch/cmd/csearch
github.com/google/codesearch/index
github.com/google/codesearch/regexp
github.com/google/codesearch/sparse
github.com/petar/GoLLRB/example
github.com/petar/GoLLRB/llrb
$

我們也可以測試這些包:

$ go test ./...
?   	github.com/google/codesearch/cmd/cgrep	[no test files]
?   	github.com/google/codesearch/cmd/cindex	[no test files]
?   	github.com/google/codesearch/cmd/csearch	[no test files]
ok  	github.com/google/codesearch/index	0.203s
ok  	github.com/google/codesearch/regexp	0.017s
?   	github.com/google/codesearch/sparse	[no test files]
?       github.com/petar/GoLLRB/example          [no test files]
ok      github.com/petar/GoLLRB/llrb             0.231s
$

如果 go 子命令在未列出路徑的情況下被呼叫,它將操作當前目錄:

$ cd github.com/google/codesearch/regexp
$ go list
github.com/google/codesearch/regexp
$ go test -v
=== RUN   TestNstateEnc
--- PASS: TestNstateEnc (0.00s)
=== RUN   TestMatch
--- PASS: TestMatch (0.00s)
=== RUN   TestGrep
--- PASS: TestGrep (0.00s)
PASS
ok  	github.com/google/codesearch/regexp	0.018s
$ go install
$

go install”子命令將包的最新副本安裝到 pkg 目錄。因為 go 命令可以分析依賴圖,“go install”還會遞迴地安裝該包所匯入但已過期的任何包。

請注意,“go install”能夠確定當前目錄中包的匯入路徑名稱,這是由於目錄命名約定。如果我們能夠選擇儲存原始碼的目錄名稱,並且我們可能不會選擇如此長的名稱,那會更方便,但這種能力會增加工具的額外配置和複雜性。鍵入一兩個額外的目錄名是為了換取更高的簡潔性和強大的功能,這是值得的。

侷限性

如上所述,go 命令不是一個通用的構建工具。特別是,它沒有任何在構建 *期間* 生成 Go 原始檔的功能,儘管它確實提供了 go generate,它可以自動化在構建 *之前* 建立 Go 檔案。對於更高階的構建設定,您可能需要編寫一個 makefile(或您選擇的構建工具的配置檔案)來執行任何建立 Go 檔案的工具,然後將這些生成的原始檔提交到您的儲存庫。這會給您(包作者)帶來更多工作,但對您的使用者來說工作量大大減少,他們可以使用“go get”而無需獲取和構建任何額外的工具。

更多資訊

有關更多資訊,請閱讀 如何編寫 Go 程式碼,並參閱 go 命令文件