初探 Go 的編譯命令執行過程

一縷殤流化隱半邊冰霜發表於2017-08-05

引言

Go 語言這兩年在語言排行榜上的上升勢頭非常猛,Go 語言雖然是靜態編譯型語言,但是它卻擁有指令碼化的語法,支援多種程式設計正規化(函式式和麵向物件)。Go 語言最最吸引人的地方可能是其原生支援併發程式設計(語言層面原生支援和通過第三方庫支援是有很大區別的)。Go 語言的對網路通訊、併發和並行程式設計的支援度極高,從而可以更好地利用大量的分散式和多核的計算機。開發者可以通過 goroutine 這種輕量級執行緒的概念來實現這個目標,然後通過 channel 來實現各個 goroutine 之間的通訊。他們實現了分段棧增長和 goroutine 線上程基礎上多路複用技術的自動化。

2017年7月 TIOBE 語言排行榜 Go 首次進入前十。今天就讓我們來探究探究 Go 的編譯命令執行過程。

一. 理解 Go 的環境變數

1. GOROOT

該環境變數的值為 Go 語言的當前安裝目錄。

2. GOPATH

該環境變數的值為 Go 語言的工作區的集合(意味著可以有很多個)。工作區類似於工作目錄。每個不同的目錄之間用分隔。

工作區是放置 Go 原始碼檔案的目錄。一般情況下,Go 原始碼檔案都需要存放到工作區中。

工作區一般會包含3個子資料夾,自己手動新建以下三個目錄:src 目錄,pkg 目錄,bin 目錄。


/home/halfrost/gorepo
├── bin
├── pkg
└── src複製程式碼

這裡需要額外說的一點:關於 IDE 新建 Go 專案。IDE 在新建完 Go 的專案以後,會自動的執行 go get 命令去把相應的基礎包拉過來,
在這個過程中會新建 bin、pkg、src 三個目錄。不用 IDE 的同學,需要自己手動建立這三個目錄。

上圖是 Atom 的 go-plus 外掛在一個新的專案開啟的時候,自動 go get 的一些基礎包。

bin 目錄裡面存放的都是通過 go install 命令安裝後,由 Go 命令原始碼檔案生成的可執行檔案( 在 Mac 平臺下是 Unix executable 檔案,在 Windows 平臺下是 exe 檔案)。

注意:有兩種情況下,bin 目錄會變得沒有意義。

  1. 當設定了有效的 GOBIN 環境變數以後,bin 目錄就變得沒有意義。
  2. 如果 GOPATH 裡面包含多個工作區路徑的時候,必須設定 GOBIN 環境變數,否則就無法安裝 Go 程式的可執行檔案。

pkg 目錄是用來存放通過 go install 命令安裝後的程式碼包的歸檔檔案(.a 檔案)。歸檔檔案的名字就是程式碼包的名字。所有歸檔檔案都會被存放到該目錄下的平臺相關目錄中,即在 $GOPATH\/pkg\/$GOOS_$GOARCH 中,同樣以程式碼包為組織形式。

這裡有兩個隱藏的環境變數,GOOS 和 GOARCH。這兩個環境變數是不用我們設定的,系統就預設的。GOOS 是 Go 所在的作業系統型別,GOARCH 是 Go 所在的計算架構。平臺相關目錄是以
$GOOS_$GOARCH 命名的,Mac 平臺上這個目錄名就是 darwin_amd64。

src 目錄是以程式碼包的形式組織並儲存 Go 原始碼檔案的。每個程式碼包都和 src 目錄下的資料夾一一對應。每個子目錄都是一個程式碼包。

這裡有一個特例,命令原始碼檔案並不一定必須放在 src 資料夾中的。

這裡需要糾正一個錯誤的觀點:“所有的 Go 的程式碼都要放在 GOPATH 目錄下”(這個觀點是錯誤的)

說到這裡需要談到 Go 的原始碼檔案分類:

如上圖,分為三類:

(1)命令原始碼檔案:

宣告自己屬於 main 程式碼包、包含無引數宣告和結果宣告的 main 函式。

命令原始碼檔案被安裝以後,GOPATH 如果只有一個工作區,那麼相應的可執行檔案會被存放當前工作區的 bin 資料夾下;如果有多個工作區,就會安裝到 GOBIN 指向的目錄下。

命令原始碼檔案是 Go 程式的入口。

同一個程式碼包中最好也不要放多個命令原始碼檔案。多個命令原始碼檔案雖然可以分開單獨 go run 執行起來,但是無法通過 go build 和 go install。


YDZ ~/LeetCode_Go/helloworld/src/me $  ls
helloworld.go  helloworldd.go複製程式碼

先說明一下,在上述資料夾中放了兩個命令原始碼檔案,同時都宣告自己屬於 main 程式碼包。helloworld.go 檔案輸出 hello world,helloworldd.go 檔案輸出 worldd hello。接下來執行 go build 和 go install ,看看會發生什麼。


YDZ ~/LeetCode_Go/helloworld/src/me $  go build
# _/Users/YDZ/LeetCode_Go/helloworld/src/me
./helloworldd.go:7: main redeclared in this block
    previous declaration at ./helloworld.go:50

YDZ ~/LeetCode_Go/helloworld/src/me $  go install
# _/Users/YDZ/LeetCode_Go/helloworld/src/me
./helloworldd.go:7: main redeclared in this block
    previous declaration at ./helloworld.go:50複製程式碼

這也就證明了多個命令原始碼檔案雖然可以分開單獨 go run 執行起來,但是無法通過 go build 和 go install。

同理,如果命令原始碼檔案和庫原始碼檔案也會出現這樣的問題,庫原始碼檔案不能通過 go build 和 go install 這種常規的方法編譯和安裝。具體例子和上述類似,這裡就不再貼程式碼了。

所以命令原始碼檔案應該是被單獨放在一個程式碼包中。

(2)庫原始碼檔案

庫原始碼檔案就是不具備命令原始碼檔案上述兩個特徵的原始碼檔案。存在於某個程式碼包中的普通的原始碼檔案。

庫原始碼檔案被安裝後,相應的歸檔檔案(.a 檔案)會被存放到當前工作區的 pkg 的平臺相關目錄下。

(3)測試原始碼檔案

名稱以 _test.go 為字尾的程式碼檔案,並且必須包含 Test 或者 Benchmark 名稱字首的函式。


func TestXXX( t *testing.T) {

}複製程式碼

名稱以 Test 為名稱字首的函式,只能接受 *testing.T 的引數,這種測試函式是功能測試函式。


func BenchmarkXXX( b *testing.B) {

}複製程式碼

名稱以 Benchmark 為名稱字首的函式,只能接受 *testing.B 的引數,這種測試函式是效能測試函式。

現在答案就很明顯了:

命令原始碼檔案是可以單獨執行的。可以使用 go run 命令直接執行,也可以通過 go build 或 go install 命令得到相應的可執行檔案。所以命令原始碼檔案是可以在機器的任何目錄下執行的。

舉個例子:

比如平時我們在 LeetCode 上刷演算法題,這時候寫的就是一個程式,這就是命令原始碼檔案,可以在電腦的任意一個資料夾新建一個 go 檔案就可以開始刷題了,寫完就可以執行,對比執行結果,答案對了就可以提交程式碼。

但是公司專案裡面的程式碼就不能這樣了,只能存放在 GOPATH 目錄下。因為公司專案不可能只有命令原始碼檔案的,肯定是包含庫原始碼檔案,甚至包含測試原始碼檔案的。

3.GOBIN

該環境變數的值為 Go 程式的可執行檔案的目錄。

4.PATH

為了方便使用 Go 語言命令和 Go 程式的可執行檔案,需要新增其值。追加的操作還是用分隔。


export PATH=$PATH:$GOBIN複製程式碼

以上就是關於 Go 的4個重要環境變數的理解。還有一些其他的環境變數,用 go env 命令就可以檢視。


YDZ ~ $  go env
GOARCH="amd64"
GOBIN="/Users/YDZ/Ele_Project/clairstormeye/bin"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/YDZ/Ele_Project/clairstormeye"
GORACE=""
GOROOT="/usr/local/Cellar/go/1.8.3/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/66/dcf61ty92rgd_xftrsxgx5yr0000gn/T/go-build977187889=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"
PKG_CONFIG="pkg-config"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"複製程式碼
名稱 說明
CGO_ENABLED 指明 cgo 工具是否可用的標識
GOARCH 程式構建環境的目標計算架構
GOBIN 存放可執行檔案的目錄的絕對路徑
GOCHAR 程式構建環境的目標計算架構的單字元標識
GOEXE 可執行檔案的字尾
GOHOSTARCH 程式執行環境的目標計算架構
GOOS 程式構建環境的目標作業系統
GOHOSTOS 程式執行環境的目標作業系統
GOPATH 工作區目錄的絕對路徑
GORACE 用於資料競爭檢測的相關選項
GOROOT Go 語言的安裝目錄的絕對路徑
GOTOOLDIR Go 工具目錄的絕對路徑

在探索 Go 的編譯命令之前,需要說明的一點是:

Go 程式是通過 package 來組織的。

package (假設我們的例子中是 package main)這一行告訴我們當前檔案屬於哪個包,而包名 main 則告訴我們它是一個可獨立執行的包,它在編譯後會產生可執行檔案。除了 main 包之外,其它的包最後都會生成 *.a 檔案(也就是包檔案)並放置在 $GOPATH/pkg/$GOOS_$GOARCH中(以 Mac 為例就是
$GOPATH/pkg/darwin_amd64 )。

Go 使用 package(和 Python 的模組類似)來組織程式碼。main.main() 函式(這個函式位於主包)是每一個獨立的可執行程式的入口點。

每一個可獨立執行的 Go 程式,必定包含一個 package main,在這個 main 包中必定包含一個入口函式 main,而這個函式既沒有引數,也沒有返回值。

二. 初探 Go 的編譯過程

目前 Go 最新版1.8.3裡面基本命令只有以下的16個。


    build       compile packages and dependencies
    clean       remove object files
    doc         show documentation for package or symbol
    env         print Go environment information
    bug         start a bug report
    fix         run go tool fix on packages
    fmt         run gofmt on package sources
    generate    generate Go files by processing source
    get         download and install packages and dependencies
    install     compile and install packages and dependencies
    list        list packages
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         run go tool vet on packages複製程式碼

其中和編譯相關的有 build、get、install、run 這4個。接下來就依次看看這四個的作用。

在詳細分析這4個命令之前,先羅列一下通用的命令標記,以下這些命令都可適用的:

名稱 說明
-a 用於強制重新編譯所有涉及的 Go 語言程式碼包(包括 Go 語言標準庫中的程式碼包),即使它們已經是最新的了。該標記可以讓我們有機會通過改動底層的程式碼包做一些實驗。
-n 使命令僅列印其執行過程中用到的所有命令,而不去真正執行它們。如果不只想檢視或者驗證命令的執行過程,而不想改變任何東西,使用它正好合適。
-race 用於檢測並報告指定 Go 語言程式中存在的資料競爭問題。當用 Go 語言編寫併發程式的時候,這是很重要的檢測手段之一。
-v 用於列印命令執行過程中涉及的程式碼包。這一定包括我們指定的目的碼包,並且有時還會包括該程式碼包直接或間接依賴的那些程式碼包。這會讓你知道哪些程式碼包被執行過了。
-work 用於列印命令執行時生成和使用的臨時工作目錄的名字,且命令執行完成後不刪除它。這個目錄下的檔案可能會對你有用,也可以從側面瞭解命令的執行過程。如果不新增此標記,那麼臨時工作目錄會在命令執行完畢前刪除。
-x 使命令列印其執行過程中用到的所有命令,並同時執行它們。

1. go run

專門用來執行命令原始碼檔案的命令,注意,這個命令不是用來執行所有 Go 的原始碼檔案的!

go run 命令只能接受一個命令原始碼檔案以及若干個庫原始碼檔案(必須同屬於 main 包)作為檔案引數,且不能接受測試原始碼檔案。它在執行時會檢查原始碼檔案的型別。如果引數中有多個或者沒有命令原始碼檔案,那麼 go run 命令就只會列印錯誤提示資訊並退出,而不會繼續執行。

這個命令具體幹了些什麼事情呢?來分析分析:


YDZ ~/LeetCode_Go/helloworld/src/me $  go run -n helloworld.go

#
# command-line-arguments
#

mkdir -p $WORK/command-line-arguments/_obj/
mkdir -p $WORK/command-line-arguments/_obj/exe/
cd /Users/YDZ/LeetCode_Go/helloworld/src/me
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -p main -complete -buildid 2841ae50ca62b7a3671974e64d76e198a2155ee7 -D _/Users/YDZ/LeetCode_Go/helloworld/src/me -I $WORK -pack ./helloworld.go
cd .
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/link -o $WORK/command-line-arguments/_obj/exe/helloworld -L $WORK -w -extld=clang -buildmode=exe -buildid=2841ae50ca62b7a3671974e64d76e198a2155ee7 $WORK/command-line-arguments.a
$WORK/command-line-arguments/_obj/exe/helloworld複製程式碼

這裡可以看到建立了兩個臨時資料夾 _obj 和 exe,先執行了 compile 命令,然後 link,生成了歸檔檔案.a 和 最終可執行檔案,最終的可執行檔案放在 exe 資料夾裡面。命令的最後一步就是執行了可執行檔案。

總結一下如下圖:

舉個例子,生成的臨時檔案可以用go run -work看到,比如當前生成的臨時資料夾是如下的路徑:


/var/folders/66/dcf61ty92rgd_xftrsxgx5yr0000gn/T/go-build876472071複製程式碼

列印目錄結構:


├── command-line-arguments
│   └── _obj
│       └── exe
│           └── helloworld
└── command-line-arguments.a複製程式碼

可以看到,最終go run命令是生成了2個檔案,一個是歸檔檔案,一個是可執行檔案。command-line-arguments 這個歸檔檔案是 Go 語言為命令原始碼檔案臨時指定的一個程式碼包。在接下來的幾個命令中,生成的臨時程式碼包都叫這個名字。

go run 命令在第二次執行的時候,如果發現匯入的程式碼包沒有發生變化,那麼 go run 不會再次編譯這個匯入的程式碼包。直接靜態連結進來。


go run -a複製程式碼

加上-a的標記可以強制編譯所有的程式碼,即使歸檔檔案.a存在,也會重新編譯。

如果嫌棄編譯速度慢,可以加上-p n,這個是並行編譯,n是並行的數量。n一般為邏輯 CPU 的個數。

2. go build

當程式碼包中有且僅有一個命令原始碼檔案的時候,在資料夾所在目錄中執行 go build 命令,會在該目錄下生成一個與目錄同名的可執行檔案。


// 假設當前資料夾名叫 myGoRepo

YDZ:~/helloworld/src/myGoRepo $ ls
helloworld.go
YDZ:~/helloworld/src/myGoRepo $ go build
YDZ:~/helloworld/src/myGoRepo $ ls
helloworld.go  myGoRepo複製程式碼

於是在當前目錄直接生成了以當前資料夾為名的可執行檔案( 在 Mac 平臺下是 Unix executable 檔案,在 Windows 平臺下是 exe 檔案)

我們先記錄一下這個可執行檔案的 md5 值


YDZ ~/helloworld/src/myGoRepo $  md5 /Users/YDZ/helloworld/src/myGoRepo/myGoRepo
MD5 (/Users/YDZ/helloworld/src/myGoRepo/myGoRepo) = 1f23f6efec752ed34b9bd22b5fa1ddce複製程式碼

但是這種情況下,如果使用 go install 命令,如果 GOPATH 裡面只有一個工作區,就會在當前工作區的 bin 目錄下生成相應的可執行檔案。如果 GOPATH 下有多個工作區,則是在 GOBIN 下生成對應的可執行檔案。

我們們先接著剛剛 go build 繼續操作。


YDZ:~/helloworld/src/myGoRepo $ ls
helloworld.go myGoRepo
YDZ:~/helloworld/src/myGoRepo $ go install
YDZ:~/helloworld/src/myGoRepo $ ls
helloworld.go複製程式碼

執行完 go install 會發現可執行檔案不見了!去哪裡了呢?其實是被移動到了 bin 目錄下了(如果 GOPATH 下有多個工作區,就會放在
GOBIN 目錄下)。


YDZ:~/helloworld/bin $ ls
myGoRepo複製程式碼

再來比對一下這個檔案的 md5 值:


YDZ ~/helloworld/bin $  md5 /Users/YDZ/helloworld/bin/myGoRepo
MD5 (/Users/YDZ/helloworld/bin/myGoRepo) = 1f23f6efec752ed34b9bd22b5fa1ddce複製程式碼

和 go build 命令執行出來的可執行檔案完全一致。我們可以大膽猜想,是把剛剛 go build 命令執行出來的可執行檔案移動到了 bin 目錄下(如果 GOPATH 下有多個工作區,就會放在 GOBIN 目錄下)。

那 go build 和 go install 究竟幹了些什麼呢?

這個問題一會再來解釋,先來說說 go build。

go build 用於編譯我們指定的原始碼檔案或程式碼包以及它們的依賴包。,但是注意如果用來編譯非命令原始碼檔案,即庫原始碼檔案,go build 執行完是不會產生任何結果的。這種情況下,go build 命令只是檢查庫原始碼檔案的有效性,只會做檢查性的編譯,而不會輸出任何結果檔案。

go build 編譯命令原始碼檔案,則會在該命令的執行目錄中生成一個可執行檔案,上面的例子也印證了這個過程。

go build 後面不追加目錄路徑的話,它就把當前目錄作為程式碼包並進行編譯。go build 命令後面如果跟了程式碼包匯入路徑作為引數,那麼該程式碼包及其依賴都會被編譯。

go run 的-a標記在 go build 這裡同樣奏效,go build 加了-a強制編譯所有涉及到的程式碼包,不加-a只會編譯歸檔檔案不是最新的程式碼包。

go build 使用-o標記可以指定輸出檔案(在這個示例中指的是可執行檔案)的名稱。它是最常用的一個 go build 命令標記。但需要注意的是,當使用標記-o的時候,不能同時對多個程式碼包進行編譯。

標記-i會使 go build 命令安裝那些編譯目標依賴的且還未被安裝的程式碼包。這裡的安裝意味著產生與程式碼包對應的歸檔檔案,並將其放置到當前工作區目錄的 pkg 子目錄的相應子目錄中。在預設情況下,這些程式碼包是不會被安裝的。

go build 常用的一些標記如下:

標記名稱 | 標記描述
|:-------|:-------:|
-a | 強行對所有涉及到的程式碼包(包含標準庫中的程式碼包)進行重新構建,即使它們已經是最新的了。
-n | 列印編譯期間所用到的其它命令,但是並不真正執行它們。
-p n | 指定編譯過程中執行各任務的並行數量(確切地說應該是併發數量)。在預設情況下,該數量等於CPU的邏輯核數。但是在darwin/arm平臺(即iPhone和iPad所用的平臺)下,該數量預設是1
-race | 開啟競態條件的檢測。不過此標記目前僅在linux/amd64freebsd/amd64darwin/amd64windows/amd64平臺下受到支援。
-v | 列印出那些被編譯的程式碼包的名字。
-work | 列印出編譯時生成的臨時工作目錄的路徑,並在編譯結束時保留它。在預設情況下,編譯結束時會刪除該目錄。
-x | 列印編譯期間所用到的其它命令。注意它與-n標記的區別。

go build 命令究竟做了些什麼呢?我們來列印一下每一步的執行過程。先看看命令原始碼檔案執行了 go build 幹了什麼事情。


#
# command-line-arguments
#

mkdir -p $WORK/command-line-arguments/_obj/
mkdir -p $WORK/command-line-arguments/_obj/exe/
cd /Users/YDZ/MyGitHub/LeetCode_Go/helloworld/src/me
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -p main -complete -buildid 2841ae50ca62b7a3671974e64d76e198a2155ee7 -D _/Users/YDZ/MyGitHub/LeetCode_Go/helloworld/src/me -I $WORK -pack ./helloworld.go
cd .
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/link -o $WORK/command-line-arguments/_obj/exe/a.out -L $WORK -extld=clang -buildmode=exe -buildid=2841ae50ca62b7a3671974e64d76e198a2155ee7 $WORK/command-line-arguments.a
mv $WORK/command-line-arguments/_obj/exe/a.out helloworld複製程式碼

可以看到,執行過程和 go run 大體相同,唯一不同的就是在最後一步,go run 是執行了可執行檔案,但是 go build 命令是把可執行檔案移動到了當前目錄的資料夾中。

列印看看生成的臨時資料夾的樹形結構


.
├── command-line-arguments
│   └── _obj
│       └── exe
└── command-line-arguments.a複製程式碼

和 go run 命令的結構基本一致,唯一的不同可執行檔案不在 exe 資料夾中了,被移動到了當前執行 go build 的資料夾中了。

在來看看庫原始碼檔案執行了 go build 以後幹了什麼事情:


#
# _/Users/YDZ/Downloads/goc2p-master/src/pkgtool
#

mkdir -p $WORK/_/Users/YDZ/Downloads/goc2p-master/src/pkgtool/_obj/
mkdir -p $WORK/_/Users/YDZ/Downloads/goc2p-master/src/
cd /Users/YDZ/Downloads/goc2p-master/src/pkgtool
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/_/Users/YDZ/Downloads/goc2p-master/src/pkgtool.a -trimpath $WORK -p _/Users/YDZ/Downloads/goc2p-master/src/pkgtool -complete -buildid cef542c3da6d3126cdae561b5f6e1470aff363ba -D _/Users/YDZ/Downloads/goc2p-master/src/pkgtool -I $WORK -pack ./envir.go ./fpath.go ./ipath.go ./pnode.go ./util.go複製程式碼

這裡可以看到 go build 命令只是把庫原始碼檔案編譯了一遍,其他什麼事情都沒有幹。

再看看生成的臨時資料夾的樹形結構


.
└── _
    └── Users
        └── YDZ
            └── Downloads
                └── goc2p-master
                    └── src
                        ├── pkgtool
                        │   └── _obj
                        └── pkgtool.a複製程式碼

可以看到它的目錄結構層級前段部分是該程式碼包所在本機的路徑的相對路徑。然後生成了歸檔檔案 .a 檔案。

總結一下如下圖:

關於 go build 和 go install 的不同,接下來分析完 go install 就會明白了,接下來繼續看 go install。

3. go install

go install 命令是用來編譯並安裝程式碼包或者原始碼檔案的。

go install 用於編譯並安裝指定的程式碼包及它們的依賴包。當指定的程式碼包的依賴包還沒有被編譯和安裝時,該命令會先去處理依賴包。與 go build 命令一樣,傳給 go install 命令的程式碼包引數應該以匯入路徑的形式提供。並且,go build 命令的絕大多數標記也都可以用於
go install 命令。實際上,go install 命令只比 go build 命令多做了一件事,即:安裝編譯後的結果檔案到指定目錄。

安裝程式碼包會在當前工作區的 pkg 的平臺相關目錄下生成歸檔檔案(即 .a 檔案)。
安裝命令原始碼檔案會在當前工作區的 bin 目錄(如果 GOPATH 下有多個工作區,就會放在 GOBIN 目錄下)生成可執行檔案。

同樣,go install 命令如果後面不追加任何引數,它會把當前目錄作為程式碼包並安裝。這和 go build 命令是完全一樣的。

go install 命令後面如果跟了程式碼包匯入路徑作為引數,那麼該程式碼包及其依賴都會被安裝。

go install 命令後面如果跟了命令原始碼檔案以及相關庫原始碼檔案作為引數的話,只有這些檔案會被編譯並安裝。

go install 命令究竟做了些什麼呢?我們來列印一下每一步的執行過程。


#
# command-line-arguments
#

mkdir -p $WORK/command-line-arguments/_obj/
mkdir -p $WORK/command-line-arguments/_obj/exe/
cd /Users/YDZ/MyGitHub/LeetCode_Go/helloworld/src/me
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -p main -complete -buildid 2841ae50ca62b7a3671974e64d76e198a2155ee7 -D _/Users/YDZ/MyGitHub/LeetCode_Go/helloworld/src/me -I $WORK -pack ./helloworld.go
cd .
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/link -o $WORK/command-line-arguments/_obj/exe/a.out -L $WORK -extld=clang -buildmode=exe -buildid=2841ae50ca62b7a3671974e64d76e198a2155ee7 $WORK/command-line-arguments.a
mkdir -p /Users/YDZ/Ele_Project/clairstormeye/bin/
mv $WORK/command-line-arguments/_obj/exe/a.out /Users/YDZ/Ele_Project/clairstormeye/bin/helloworld複製程式碼

前面幾步依舊和 go run 、go build 完全一致,只是最後一步的差別,go install 會把命令原始碼檔案安裝到當前工作區的 bin 目錄(如果 GOPATH 下有多個工作區,就會放在 GOBIN 目錄下)。如果是庫原始碼檔案,就會被安裝到當前工作區的 pkg 的平臺相關目錄下。

還是來看看 go install 生成的臨時資料夾的結構:


.
├── command-line-arguments
│   └── _obj
│       └── exe
└── command-line-arguments.a複製程式碼

結構和執行了 go build 命令一樣,最終生成的檔案也都被移動到了相對應的目標目錄中。

總結一下如下圖:

在安裝多個庫原始碼檔案時有可能遇到如下的問題:

hc@ubt:~/golang/goc2p/src/pkgtool$ go install envir.go fpath.go ipath.go pnode.go util.go
go install: no install location for .go files listed on command line (GOBIN not set)複製程式碼

而且,在我們為環境變數 GOBIN 設定了正確的值之後,這個錯誤提示資訊仍然會出現。這是因為,只有在安裝命令原始碼檔案的時候,命令程式才會將環境變數 GOBIN 的值作為結果檔案的存放目錄。而在安裝庫原始碼檔案時,在命令程式內部的代表結果檔案存放目錄路徑的那個變數不會被賦值。最後,命令程式會發現它依然是個無效的空值。所以,命令程式會同樣返回一個關於“無安裝位置”的錯誤。這就引出一個結論,我們只能使用安裝程式碼包的方式來安裝庫原始碼檔案,而不能在 go install 命令羅列並安裝它們。另外,go install 命令目前無法接受標記-o以自定義結果檔案的存放位置。這也從側面說明了
go install 命令不支援針對庫原始碼檔案的安裝操作。

4. go get

go get 命令用於從遠端程式碼倉庫(比如 Github )上下載並安裝程式碼包。注意,go get 命令會把當前的程式碼包下載到 $GOPATH 中的第一個工作區的 src 目錄中,並安裝。

如果在 go get 下載過程中加入-d 標記,那麼下載操作只會執行下載動作,而不執行安裝動作。比如有些非常特殊的程式碼包在安裝過程中需要有特殊的處理,所以我們需要先下載下來,所以就會用到-d 標記。

還有一個很有用的標記是-u標記,加上它可以利用網路來更新已有的程式碼包及其依賴包。如果已經下載過一個程式碼包,但是這個程式碼包又有更新了,那麼這時候可以直接用-u標記來更新本地的對應的程式碼包。如果不加這個-u標記,執行 go get 一個已有的程式碼包,會發現命令什麼都不執行。只有加了-u標記,命令會去執行 git pull 命令拉取最新的程式碼包的最新版本,下載並安裝。

命令 go get 還有一個很值得稱道的功能——智慧下載。在使用它檢出或更新程式碼包之後,它會尋找與本地已安裝 Go 語言的版本號相對應的標籤(tag)或分支(branch)。比如,本機安裝 Go 語言的版本是1.x,那麼 go get 命令會在該程式碼包的遠端倉庫中尋找名為 “go1” 的標籤或者分支。如果找到指定的標籤或者分支,則將原生程式碼包的版本切換到此標籤或者分支。如果沒有找到指定的標籤或者分支,則將原生程式碼包的版本切換到主幹的最新版本。

go get 常用的一些標記如下:

標記名稱 標記描述
-d 讓命令程式只執行下載動作,而不執行安裝動作。
-f 僅在使用-u標記時才有效。該標記會讓命令程式忽略掉對已下載程式碼包的匯入路徑的檢查。如果下載並安裝的程式碼包所屬的專案是你從別人那裡 Fork 過來的,那麼這樣做就尤為重要了。
-fix 讓命令程式在下載程式碼包後先執行修正動作,而後再進行編譯和安裝。
-insecure 允許命令程式使用非安全的 scheme(如 HTTP )去下載指定的程式碼包。如果你用的程式碼倉庫(如公司內部的 Gitlab )沒有HTTPS 支援,可以新增此標記。請在確定安全的情況下使用它。
-t 讓命令程式同時下載並安裝指定的程式碼包中的測試原始碼檔案中依賴的程式碼包。
-u 讓命令利用網路來更新已有程式碼包及其依賴包。預設情況下,該命令只會從網路上下載本地不存在的程式碼包,而不會更新已有的程式碼包。

go get 命令究竟做了些什麼呢?我們還是來列印一下每一步的執行過程。



cd .
git clone https://github.com/go-errors/errors /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
cd /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
git submodule update --init --recursive
cd /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
git show-ref
cd /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
git submodule update --init --recursive
WORK=/var/folders/66/dcf61ty92rgd_xftrsxgx5yr0000gn/T/go-build124856678
mkdir -p $WORK/github.com/go-errors/errors/_obj/
mkdir -p $WORK/github.com/go-errors/
cd /Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors
/usr/local/Cellar/go/1.8.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/github.com/go-errors/errors.a -trimpath $WORK -p github.com/go-errors/errors -complete -buildid bb3526a8c1c21853f852838637d531b9fcd57d30 -D _/Users/YDZ/Ele_Project/clairstormeye/src/github.com/go-errors/errors -I $WORK -pack ./error.go ./parse_panic.go ./stackframe.go
mkdir -p /Users/YDZ/Ele_Project/clairstormeye/pkg/darwin_amd64/github.com/go-errors/
mv $WORK/github.com/go-errors/errors.a /Users/YDZ/Ele_Project/clairstormeye/pkg/darwin_amd64/github.com/go-errors/errors.a複製程式碼

這裡可以很明顯的看到,執行完 go get 命令以後,會呼叫 git clone 方法下載原始碼,並編譯,最終會把庫原始碼檔案編譯成歸檔檔案安裝到 pkg 對應的相關平臺目錄下。

總結一下如下圖:

關於工作區的問題,這裡額外提一下:

一般情況下,為了分離自己與第三方的程式碼,我們會設定兩個或更多的工作區。我們現在有一個目錄路徑為 /home/hc/golang/lib 的工作區,並且它是環境變數 GOPATH 值中的第一個目錄路徑。注意,環境變數 GOPATH 中包含的路徑不能與環境變數GOROOT的值重複。好了,如果我們使用 go get 命令下載和安裝程式碼包,那麼這些程式碼包都會被安裝在上面這個工作區中。我們暫且把這個工作區叫做
Lib 工作區。

三. 靜態連結 or 動態連結 ?

Go 在最初剛剛釋出的時候,靜態連結被當做優點宣傳,只須編譯後的一個可執行檔案,無須附加任何東西就能部署。將執行時、依賴庫直接打包到可執行檔案內部,簡化了部署和釋出的操作,無須事先安裝執行環境和下載諸多第三方庫。不過最新版本卻又加入了動態連結的內容了。

普通的 go build 、go install 用的都是靜態連結。可以驗證一下:

上圖是筆者用 MachOView 開啟的 gofmt 檔案,可以看到 fmt.Println 的地址是確定的,所以可以確定是靜態連結的。

目前最新版的 Go 是如何支援動態連結的呢?

在 go build 、go install 的時候加上 -buildmode 引數。

這些是以下 buildmode 的選項:

archive: 將非 main 包構建為 .a 檔案 . main 包將被忽略。
c-archive: 將 main 軟體包及其匯入的所有軟體包構建到 C 歸檔檔案中
c-shared: 將列出的主要軟體包,以及它們匯入的所有軟體包構建到
C 動態庫中。
shared: 將所有列出的非 main 軟體包合併到一個動態庫中。
exe: 構建列出的 main 包及其匯入到可執行檔案中的一切。 將忽略未命名為 main 的包。
預設情況下,列出的 main 軟體包內建到可執行檔案中,列出的非
main 軟體包內建到 .a 檔案中。

關於動態庫,筆者還沒有實踐過,這裡就不繼續深入了,以後充分實踐後,再開一篇單獨的文章談談 Go 的動態連結。這裡只是想說明一點,Go 目前不僅僅只有靜態連結,動態連結也支援了!


Reference:
《GO 命令教程》
《Go 併發程式設計實戰》

GitHub Repo:Halfrost-Field

Follow: halfrost · GitHub

Source: halfrost.com/go_command/

相關文章