# go build
`go build`命令用于編譯我們指定的源碼文件或代碼包以及它們的依賴包。
例如,如果我們在執行`go build`命令時不后跟任何代碼包,那么命令將試圖編譯當前目錄所對應的代碼包。例如,我們想編譯goc2p項目的代碼包`logging`。其中一個方法是進入`logging`目錄并直接執行該命令:
```bash
hc@ubt:~/golang/goc2p/src/logging$ go build
```
因為在代碼包`logging`中只有庫源碼文件和測試源碼文件,所以在執行`go build`命令之后不會在當前目錄和goc2p項目的pkg目錄中產生任何文件。
**插播:**Go語言的源碼文件有三大類,即:命令源碼文件、庫源碼文件和測試源碼文件。他們的功用各不相同,而寫法也各有各的特點。命令源碼文件總是作為可執行的程序的入口。庫源碼文件一般用于集中放置各種待被使用的程序實體(全局常量、全局變量、接口、結構體、函數等等)。而測試源碼文件主要用于對前兩種源碼文件中的程序實體的功能和性能進行測試。另外,后者也可以用于展現前兩者中程序的使用方法。
另外一種編譯`logging`包的方法是:
```bash
hc@ubt:~/golang/goc2p/src$ go build logging
```
在這里,我們把代碼包`logging`的導入路徑作為參數傳遞給`go build`命令。另一個例子:如果我們要編譯代碼包`cnet/ctcp`,只需要在任意目錄下執行命令`go build cnet/ctcp`即可。
**插播:**之所以這樣的編譯方法可以正常執行,是因為我們已經在環境變量`GOPATH`中加入了goc2p項目的根目錄(即`~/golang/goc2p/`)。這時,goc2p項目的根目錄就成為了一個工作區目錄。只有這樣,Go語言才能正確識別我們提供的goc2p項目中某個代碼包的導入路徑。而代碼包的導入路徑是指,相對于Go語言自身的源碼目錄(即`$GOROOT/src`)或我們在環境變量`GOPATH`中指定的某個目錄的`src`子目錄下的子路徑。例如,這里的代碼包`logging`的絕對路徑是`~/golang/goc2p/src/logging`。而不論goc2p項目的根文件夾被放在哪兒,`logging`包的導入路徑都是`logging`。顯而易見,我們在稱呼一個代碼包的時候總是以其導入路徑作為其稱謂。
言歸正傳,除了上面的簡單用法,我們還可以同時編譯多個Go源碼文件:
```bash
hc@ubt:~/golang/goc2p/src$ go build logging/base.go logging/console_logger.go logging/log_manager.go logging/tag.go
```
但是,使用這種方法會有一個限制。作為參數的多個Go源碼文件必須在同一個目錄中。也就是說,如果我們想用這種方法既編譯`logging`包又編譯`basic`包是不可能的。不過別擔心,在需要的時候,那些被編譯目標依賴的代碼包會被`go build`命令自動的編譯。例如,如果有一個導入路徑為`app`的代碼包,同時依賴了`logging`包和`basic`包。那么在執行`go build app`的時候,該命令就會自動的在編譯`app`包之前去檢查`logging`包和`basic`包的編譯狀態。如果發現它們的編譯結果文件不是最新的,那么該命令就會先去的編譯這兩個代碼包,然后再編譯`app`包。
注意,`go build`命令在編譯只包含庫源碼文件的代碼包(或者同時編譯多個代碼包)時,只會做檢查性的編譯,而不會輸出任何結果文件。
另外,`go build`命令既不能編譯包含多個命令源碼文件的代碼包,也不能同時編譯多個命令源碼文件。因為,如果把多個命令源碼文件作為一個整體看待,那么每個文件中的main函數就屬于重名函數,在編譯時會拋出重復定義錯誤。假如,在goc2p項目的代碼包`cmd`(此代碼包僅用于示例目的,并不會永久存在于該項目中)中包含有兩個命令源碼文件showds.go和initpkg_demo.go,那么我們在使用`go build`命令同時編譯它們時就會失敗。示例如下:
```bash
hc@ubt:~/golang/goc2p/src/cmd$ go build showds.go initpkg_demo.go
# command-line-arguments
./initpkg_demo.go:19: main redeclared in this block
previous declaration at ./showds.go:56
```
請注意上面示例中的`command-line-arguments`。在這個位置上應該顯示的是作為編譯目標的源碼文件所屬的代碼包的導入路徑。但是,這里顯示的并不是它們所屬的代碼包的導入路徑`cmd`。這是因為,命令程序在分析參數的時候如果發現第一個參數是Go源碼文件而不是代碼包,則會在內部生成一個虛擬代碼包。這個虛擬代碼包的導入路徑和名稱都會是`command-line-arguments`。在其他基于編譯流程的命令程序中也有與之一致的操作,比如`go install`命令和`go run`命令。
另一方面,如果我們編譯的多個屬于`main`包的源碼文件中沒有`main`函數的聲明,那么就會使編譯器立即報出“未定義`main`函數聲明”的錯誤并中止編譯。換句話說,在我們同時編譯多個`main`包的源碼文件時,要保證其中有且僅有一個`main`函數聲明,否則編譯是無法成功的。
現在我們使用`go build`命令編譯單一命令源碼文件。我們在執行命令時加入一個標記`-v`。這個標記的意義在于可以使命令把執行過程中構建的包名打印出來。我們會在稍后對這個標記進行詳細說明。現在我們先來看一個示例:
```bash
hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
initpkg_demo.go
hc@ubt:~/golang/goc2p/src/basic/pkginit$ go build -v initpkg_demo.go
command-line-arguments
hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
initpkg_demo initpkg_demo.go
```
我們在執行命令`go build -v initpkg_demo.go`之后被打印出的`command-line-arguments`”`就是命令程序為命令源碼文件initpkg_demo.go生成的虛擬代碼包的包名。順帶說一句,
命令`go build`會把編譯命令源碼文件后生成的結果文件存放到執行該命令時所在的目錄下。這個所說的結果文件就是與命令源碼文件對應的可執行文件。它的名稱會與命令源碼文件的主文件名相同。
順便說一下,如果我們有多個聲明為屬于`main`包的源碼文件,且其中只有一個文件聲明了`main`函數的話,那么是可以使用`go build`命令同時編譯它們的。在這種情況下,不包含`main`函數聲明的那幾個源碼文件會被視為庫源碼文件(理所當然)。如此編譯之后的結果文件的名稱將會與我們指定的編譯目標中最左邊的那個源碼文件的主文件名相同。
其實,除了讓Go語言編譯器自行決定可執行文件的名稱,我們還可以自定義它。示例如下:
```bash
hc@ubt:~/golang/goc2p/src/basic/pkginit$ go build -o initpkg initpkg_demo.go
hc@ubt:~/golang/goc2p/src/basic/pkginit$ ls
initpkg initpkg_demo.go
```
使用`-o`標記可以指定輸出文件(在這個示例中指的是可執行文件)的名稱。它是最常用的一個`go build`命令標記。但需要注意的是,當使用標記`-o`的時候,不能同時對多個代碼包進行編譯。
標記`-i`會使`go build`命令安裝那些編譯目標依賴的且還未被安裝的代碼包。這里的安裝意味著產生與代碼包對應的歸檔文件,并將其放置到當前工作區目錄的`pkg`子目錄的相應子目錄中。在默認情況下,這些代碼包是不會被安裝的。
除此之外,還有一些標記不但受到`go build`命令的支持,而且對于后面會提到的`go install`、`go run`、`go test`等命令同樣是有效的。下表列出了其中比較常用的標記。
_表0-1 `go build`命令的常用標記說明_
標記名稱 | 標記描述
------------ | -------------
-a | 強行對所有涉及到的代碼包(包含標準庫中的代碼包)進行重新構建,即使它們已經是最新的了。
-n | 打印編譯期間所用到的其它命令,但是并不真正執行它們。
-p n | 指定編譯過程中執行各任務的并行數量(確切地說應該是并發數量)。在默認情況下,該數量等于CPU的邏輯核數。但是在`darwin/arm`平臺(即iPhone和iPad所用的平臺)下,該數量默認是`1`。
-race | 開啟競態條件的檢測。不過此標記目前僅在`linux/amd64`、`freebsd/amd64`、`darwin/amd64`和`windows/amd64`平臺下受到支持。
-v | 打印出那些被編譯的代碼包的名字。
-work | 打印出編譯時生成的臨時工作目錄的路徑,并在編譯結束時保留它。在默認情況下,編譯結束時會刪除該目錄。
-x | 打印編譯期間所用到的其它命令。注意它與`-n`標記的區別。
我們在這里忽略了一些并不常用的或作用于編譯器或連接器的標記。在本小節的最后將會對這些標記進行簡單的說明。如果讀者有興趣,也可以查看Go語言的官方文檔以獲取相關信息。
下面我們就用其中幾個標記來查看一下在構建代碼包`logging`時創建的臨時工作目錄的路徑:
```bash
hc@ubt:~/golang/goc2p/src$ go build -v -work logging
WORK=/tmp/go-build888760008
logging
```
上面命令的結果輸出的第一行是為了編譯`logging`包,Go創建的一個臨時工作目錄,這個目錄被創建到了Linux的臨時目錄下。輸出的第二行是對標記`-v`的響應。這意味著此次命令執行時僅編譯了`logging`包。關于臨時工作目錄的用途和內容,我們會在講解`go run`命令和`go test`命令的時候詳細說明。
現在我們再來看看如果強制重新編譯會涉及到哪些代碼包:
```bash
hc@ubt:~/golang/goc2p/src$ go build -a -v -work logging
WORK=/tmp/go-build929017331
runtime
errors
sync/atomic
math
unicode/utf8
unicode
sync
io
syscall
strings
time
strconv
reflect
os
fmt
log
logging
```
怎么會多編譯了這么多代碼包呢?可以確定的是,代碼包`logging`中的代碼直接依賴了標準庫中的`runtime`包、`strings`包、`fmt`包和`log`包。那么其他的代碼包為什么也會被重新編譯呢?
從代碼包編譯的角度來說,如果代碼包A依賴代碼包B,則稱代碼包B是代碼包A的依賴代碼包(以下簡稱依賴包),代碼包A是代碼包B的觸發代碼包(以下簡稱觸發包)。
`go build`命令在執行時,編譯程序會先查找目標代碼包的所有依賴包,以及這些依賴包的依賴包,直至找到最深層的依賴包為止。在此過程中,如果發現有循環依賴的情況,編譯程序就會輸出錯誤信息并立即退出。此過程完成之后,所有的依賴關系也就形成了一棵含有重復元素的依賴樹。對于依賴樹中的一個節點(代碼包)來說,它的直接分支節點(前者的依賴包),是按照代碼包導入路徑的字典序從左到右排列的。最左邊的分支節點會最先被編譯。編譯程序會依此設定每個代碼包的編譯優先級。
執行`go build`命令的計算機如果擁有多個邏輯CPU核心,那么編譯代碼包的順序可能會存在一些不確定性。但是,它一定會滿足這樣的約束條件:`依賴代碼包 -> 當前代碼包 -> 觸發代碼包`。
標記`-p n`可以限制編譯過程中任務執行的并發數量,`n`默認為當前計算機的CPU邏輯核數。如果在執行`go build`命令時加入標記`-p 1`,那么就可以保證代碼包編譯順序嚴格按照預先設定好的優先級進行。現在我們再來編譯`logging`包:
```bash
hc@ubt:~/golang/goc2p/src$ go build -a -v -work -p 1 logging
WORK=/tmp/go-build114039681
runtime
errors
sync/atomic
sync
io
math
syscall
time
os
unicode/utf8
strconv
reflect
fmt
log
unicode
strings
logging
```
我們可以認為,以上示例中所顯示的代碼包的順序,就是`logging`包直接或間接依賴的代碼包按照優先級從高到低排列后的排序。
另外,如果在命令中加入標記`-n`,那么編譯程序只會輸出所用到的命令而不會真正運行。在這種情況下,編譯過程不會使用并發模式。
在本節的最后,我們對一些并不太常用的標記進行簡要的說明:
+ `-asmflags`
此標記可以后跟另外一些標記,如`-D`、`-I`、`-S`等。這些后跟的標記用于控制Go語言編譯器編譯匯編語言文件時的行為。
+ `-buildmode`
此標記用于指定編譯模式,使用方式如`-buildmode=default`(這等同于默認情況下的設置)。此標記支持的編譯模式目前有6種。借此,我們可以控制編譯器在編譯完成后生成靜態鏈接庫(即.a文件,也就是我們之前說的歸檔文件)、動態鏈接庫(即.so文件)或/和可執行文件(在Windows下是.exe文件)。
+ `-compiler`
此標記用于指定當前使用的編譯器的名稱。其值可以為`gc`或`gccgo`。其中,gc編譯器即為Go語言自帶的編輯器,而gccgo編譯器則為GCC提供的Go語言編譯器。而GCC則是GNU項目出品的編譯器套件。GNU是一個眾所周知的自由軟件項目。在開源軟件界不應該有人不知道它。好吧,如果你確實不知道它,趕緊去google吧。
+ `-gccgoflags`
此標記用于指定需要傳遞給gccgo編譯器或鏈接器的標記的列表。
+ `-gcflags`
此標記用于指定需要傳遞給`go tool compile`命令的標記的列表。
+ `-installsuffix`
為了使當前的輸出目錄與默認的編譯輸出目錄分離,可以使用這個標記。此標記的值會作為結果文件的父目錄名稱的后綴。其實,如果使用了`-race`標記,這個標記會被自動追加且其值會為`race`。如果我們同時使用了`-race`標記和`-installsuffix`,那么在`-installsuffix`標記的值的后面會再被追加`_race`,并以此來作為實際使用的后綴。
+ `-ldflags`
此標記用于指定需要傳遞給`go tool link`命令的標記的列表。
+ `-linkshared`
此標記用于與`-buildmode=shared`一同使用。后者會使作為編譯目標的非`main`代碼包都被合并到一個動態鏈接庫文件中,而前者則會在此之上進行鏈接操作。
+ `-pkgdir`
使用此標記可以指定一個目錄。編譯器會只從該目錄中加載代碼包的歸檔文件,并會把編譯可能會生成的代碼包歸檔文件放置在該目錄下。
+ `-tags`
此標記用于指定在實際編譯期間需要受理的編譯標簽(也可被稱為編譯約束)的列表。這些編譯標簽一般會作為源碼文件開始處的注釋的一部分,例如,在`$GOROOT/src/os/file_posix.go`開始處的注釋為:
```go
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris windows
```
最后一行注釋即包含了與編譯標簽有關的內容。大家可以查看代碼包`go/build`的文檔已獲得更多的關于編譯標簽的信息。
+ `-toolexec`
此標記可以讓我們去自定義在編譯期間使用一些Go語言自帶工具(如`vet`、`asm`等)的方式。