# 第十章 包和工具
現在隨便一個小程序的實現都可能包含超過10000個函數。然而作者一般只需要考慮其中很小的一部分和做很少的設計,因為絕大部分代碼都是由他人編寫的,它們通過類似包或模塊的方式被重用。
Go語言有超過100個的標準包(譯注:可以用`go list std | wc -l`命令查看標準包的具體數目),標準庫為大多數的程序提供了必要的基礎構件。在Go的社區,有很多成熟的包被設計、共享、重用和改進,目前互聯網上已經發布了非常多的Go語音開源包,它們可以通過 http://godoc.org 檢索。在本章,我們將演示如果使用已有的包和創建新的包。
Go還自帶了工具箱,里面有很多用來簡化工作區和包管理的小工具。在本書開始的時候,我們已經見識過如何使用工具箱自帶的工具來下載、構件和運行我們的演示程序了。在本章,我們將看看這些工具的基本設計理論和嘗試更多的功能,例如打印工作區中包的文檔和查詢相關的元數據等。在下一章,我們將探討探索包的單元測試用法。
### 10.1. 包簡介
任何包系統設計的目的都是為了簡化大型程序的設計和維護工作,通過將一組相關的特性放進一個獨立的單元以便于理解和更新,在每個單元更新的同時保持和程序中其它單元的相對獨立性。這種模塊化的特性允許每個包可以被其它的不同項目共享和重用,在項目范圍內、甚至全球范圍統一的分發和復用。
每個包一般都定義了一個不同的名字空間用于它內部的每個標識符的訪問。每個名字空間關聯到一個特定的包,讓我們給類型、函數等選擇簡短明了的名字,這樣可以避免在我們使用它們的時候減少和其它部分名字的沖突。
每個包還通過控制包內名字的可見性和是否導出來實現封裝特性。通過限制包成員的可見性并隱藏包API的具體實現,將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現。通過限制包內變量的可見性,還可以強制用戶通過某些特定函數來訪問和更新內部變量,這樣可以保證內部變量的一致性和并發時的互斥約束。
當我們修改了一個源文件,我們必須重新編譯該源文件對應的包和所有依賴該包的其他包。即使是從頭構建,Go語言編譯器的編譯速度也明顯快于其它編譯語言。Go語言的閃電般的編譯速度主要得益于三個語言特性。第一點,所有導入的包必須在每個文件的開頭顯式聲明,這樣的話編譯器就沒有必要讀取和分析整個源文件來判斷包的依賴關系。第二點,禁止包的環狀依賴,因為沒有循環依賴,包的依賴關系形成一個有向無環圖,每個包可以被獨立編譯,而且很可能是被并發編譯。第三點,編譯后包的目標文件不僅僅記錄包本身的導出信息,目標文件同時還記錄了包的依賴關系。因此,在編譯一個包的時候,編譯器只需要讀取每個直接導入包的目標文件,而不需要遍歷所有依賴的的文件(譯注:很多都是重復的間接依賴)。
### 10.2. 導入路徑
每個包是由一個全局唯一的字符串所標識的導入路徑定位。出現在import語句中的導入路徑也是字符串。
~~~
import (
"fmt"
"math/rand"
"encoding/json"
"golang.org/x/net/html"
"github.com/go-sql-driver/mysql"
)
~~~
就像我們在2.6.1節提到過的,Go語言的規范并沒有指明包的導入路徑字符串的具體含義,導入路徑的具體含義是由構建工具來解釋的。在本章,我們將深入討論Go語言工具箱的功能,包括大家經常使用的構建測試等功能。當然,也有第三方擴展的工具箱存在。例如,Google公司內部的Go語言碼農,他們就使用內部的多語言構建系統(譯注:Google公司使用的是類似[Bazel](http://bazel.io)的構建系統,支持多種編程語言,目前該構件系統還不能完整支持Windows環境),用不同的規則來處理包名字和定位包,用不同的規則來處理單元測試等等,因為這樣可以更緊密適配他們內部環境。
如果你計劃分享或發布包,那么導入路徑最好是全球唯一的。為了避免沖突,所有非標準庫包的導入路徑建議以所在組織的互聯網域名為前綴;而且這樣也有利于包的檢索。例如,上面的import語句導入了Go團隊維護的HTML解析器和一個流行的第三方維護的MySQL驅動。
### 10.3. 包聲明
在每個Go語音源文件的開頭都必須有包聲明語句。包聲明語句的主要目的是確定當前包被其它包導入時默認的標識符(也稱為包名)。
例如,math/rand包的每個源文件的開頭都包含`package rand`包聲明語句,所以當你導入這個包,你就可以用rand.Int、rand.Float64類似的方式訪問包的成員。
~~~
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Int())
}
~~~
通常來說,默認的包名就是包導入路徑名的最后一段,因此即使兩個包的導入路徑不同,它們依然可能有一個相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。稍后我們將看到如何同時導入兩個有相同包名的包。
關于默認包名一般采用導入路徑名的最后一段的約定也有三種例外情況。第一個例外,包對應一個可執行程序,也就是main包,這時候main包本身的導入路徑是無關緊要的。名字為main的包是給go build(§10.7.3)構建命令一個信息,這個包編譯完之后必須調用連接器生成一個可執行程序。
第二個例外,包所在的目錄中可能有一些文件名是以_test.go為后綴的Go源文件(譯注:前面必須有其它的字符,因為以`_`前綴的源文件是被忽略的),并且這些源文件聲明的包名也是以_test為后綴名的。這種目錄可以包含兩種包:一種普通包,加一種則是測試的外部擴展包。所有以_test為后綴包名的測試外部擴展包都由go test命令獨立編譯,普通包和測試的外部擴展包是相互獨立的。測試的外部擴展包一般用來避免測試代碼中的循環導入依賴,具體細節我們將在11.2.4節中介紹。
第三個例外,一些依賴版本號的管理工具會在導入路徑后追加版本號信息,例如"gopkg.in/yaml.v2"。這種情況下包的名字并不包含版本號后綴,而是yaml。
### 10.4. 導入聲明
可以在一個Go語言源文件包聲明語句之后,其它非導入聲明語句之前,包含零到多個導入包聲明語句。每個導入聲明可以單獨指定一個導入路徑,也可以通過圓括號同時導入多個導入路徑。下面兩個導入形式是等價的,但是第二種形式更為常見。
~~~
import "fmt"
import "os"
import (
"fmt"
"os"
)
~~~
導入的包之間可以通過添加空行來分組;通常將來自不同組織的包獨自分組。包的導入順序無關緊要,但是在每個分組中一般會根據字符串順序排列。(gofmt和goimports工具都可以將不同分組導入的包獨立排序。)
~~~
import (
"fmt"
"html/template"
"os"
"golang.org/x/net/html"
"golang.org/x/net/ipv4"
)
~~~
如果我們想同時導入兩個有著名字相同的包,例如math/rand包和crypto/rand包,那么導入聲明必須至少為一個同名包指定一個新的包名以避免沖突。這叫做導入包的重命名。
~~~
import (
"crypto/rand"
mrand "math/rand" // alternative name mrand avoids conflict
)
~~~
導入包的重命名只影響當前的源文件。其它的源文件如果導入了相同的包,可以用導入包原本默認的名字或重命名為另一個完全不同的名字。
導入包重命名是一個有用的特性,它不僅僅只是為了解決名字沖突。如果導入的一個包名很笨重,特別是在一些自動生成的代碼中,這時候用一個簡短名稱會更方便。選擇用簡短名稱重命名導入包時候最好統一,以避免包名混亂。選擇另一個包名稱還可以幫助避免和本地普通變量名產生沖突。例如,如果文件中已經有了一個名為path的變量,那么我們可以將"path"標準包重命名為pathpkg。
每個導入聲明語句都明確指定了當前包和被導入包之間的依賴關系。如果遇到包循環導入的情況,Go語言的構建工具將報告錯誤。
### 10.5. 包的匿名導入
如果只是導入一個包而并不使用導入的包將會導致一個編譯錯誤。但是有時候我們只是想利用導入包而產生的副作用:它會計算包級變量的初始化表達式和執行導入包的init初始化函數(§2.6.2)。這時候我們需要抑制“unused import”編譯錯誤,我們可以用下劃線`_`來重命名導入的包。像往常一樣,下劃線`_`為空白標識符,并不能被訪問。
~~~
import _ "image/png" // register PNG decoder
~~~
這個被稱為包的匿名導入。它通常是用來實現一個編譯時機制,然后通過在main主程序入口選擇性地導入附加的包。首先,讓我們看看如何使用該特性,然后再看看它是如何工作的。
標準庫的image圖像包包含了一個`Decode`函數,用于從`io.Reader`接口讀取數據并解碼圖像,它調用底層注冊的圖像解碼器來完成任務,然后返回image.Image類型的圖像。使用`image.Decode`很容易編寫一個圖像格式的轉換工具,讀取一種格式的圖像,然后編碼為另一種圖像格式:
*gopl.io/ch10/jpeg*
~~~
// The jpeg command reads a PNG image from the standard input
// and writes it as a JPEG image to the standard output.
package main
import (
"fmt"
"image"
"image/jpeg"
_ "image/png" // register PNG decoder
"io"
"os"
)
func main() {
if err := toJPEG(os.Stdin, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "jpeg: %v\n", err)
os.Exit(1)
}
}
func toJPEG(in io.Reader, out io.Writer) error {
img, kind, err := image.Decode(in)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Input format =", kind)
return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
}
~~~
如果我們將`gopl.io/ch3/mandelbrot`(§3.3)的輸出導入到這個程序的標準輸入,它將解碼輸入的PNG格式圖像,然后轉換為JPEG格式的圖像輸出(圖3.3)。
~~~
$ go build gopl.io/ch3/mandelbrot
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
Input format = png
~~~
要注意image/png包的匿名導入語句。如果沒有這一行語句,程序依然可以編譯和運行,但是它將不能正確識別和解碼PNG格式的圖像:
~~~
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
jpeg: image: unknown format
~~~
下面的代碼演示了它的工作機制。標準庫還提供了GIF、PNG和JPEG等格式圖像的解碼器,用戶也可以提供自己的解碼器,但是為了保持程序體積較小,很多解碼器并沒有被全部包含,除非是明確需要支持的格式。image.Decode函數在解碼時會依次查詢支持的格式列表。每個格式驅動列表的每個入口指定了四件事情:格式的名稱;一個用于描述這種圖像數據開頭部分模式的字符串,用于解碼器檢測識別;一個Decode函數用于完成解碼圖像工作;一個DecodeConfig函數用于解碼圖像的大小和顏色空間的信息。每個驅動入口是通過調用image.RegisterFormat函數注冊,一般是在每個格式包的init初始化函數中調用,例如image/png包是這樣注冊的:
~~~
package png // image/png
func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)
func init() {
const pngHeader = "\x89PNG\r\n\x1a\n"
image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
~~~
最終的效果是,主程序只需要匿名導入特定圖像驅動包就可以用image.Decode解碼對應格式的圖像了。
數據庫包database/sql也是采用了類似的技術,讓用戶可以根據自己需要選擇導入必要的數據庫驅動。例如:
~~~
import (
"database/sql"
_ "github.com/lib/pq" // enable support for Postgres
_ "github.com/go-sql-driver/mysql" // enable support for MySQL
)
db, err = sql.Open("postgres", dbname) // OK
db, err = sql.Open("mysql", dbname) // OK
db, err = sql.Open("sqlite3", dbname) // returns error: unknown driver "sqlite3"
~~~
**練習 10.1:** 擴展jpeg程序,以支持任意圖像格式之間的相互轉換,使用image.Decode檢測支持的格式類型,然后通過flag命令行標志參數選擇輸出的格式。
**練習 10.2:** 設計一個通用的壓縮文件讀取框架,用來讀取ZIP(archive/zip)和POSIX tar(archive/tar)格式壓縮的文檔。使用類似上面的注冊技術來擴展支持不同的壓縮格式,然后根據需要通過匿名導入選擇導入要支持的壓縮格式的驅動包。
### 10.6. 包和命名
在本節中,我們將提供一些關于Go語言獨特的包和成員命名的約定。
當創建一個包,一般要用短小的包名,但也不能太短導致難以理解。標準庫中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。
它們的名字都簡潔明了。例如,不要將一個類似imageutil或ioutilis的通用包命名為util,雖然它看起來很短小。要盡量避免包名使用可能被經常用于局部變量的名字,這樣可能導致用戶重命名導入包,例如前面看到的path包。
包名一般采用單數的形式。標準庫的bytes、errors和strings使用了復數形式,這是為了避免和預定義的類型沖突,同樣還有go/types是為了避免和type關鍵字沖突。
要避免包名有其它的含義。例如,2.5節中我們的溫度轉換包最初使用了temp包名,雖然并沒有持續多久。但這是一個糟糕的嘗試,因為temp幾乎是臨時變量的同義詞。然后我們有一段時間使用了temperature作為包名,雖然名字并沒有表達包的真實用途。最后我們改成了和strconv標準包類似的tempconv包名,這個名字比之前的就好多了。
現在讓我們看看如何命名包的成員。由于是通過包的導入名字引入包里面的成員,例如fmt.Println,同時包含了包名和成員名信息。因此,我們一般并不需要關注Println的具體內容,因為fmt包名已經包含了這個信息。當設計一個包的時候,需要考慮包名和成員名兩個部分如何很好地配合。下面有一些例子:
~~~
bytes.Equal flag.Int http.Get json.Marshal
~~~
我們可以看到一些常用的命名模式。strings包提供了和字符串相關的諸多操作:
~~~
package strings
func Index(needle, haystack string) int
type Replacer struct{ /* ... */ }
func NewReplacer(oldnew ...string) *Replacer
type Reader struct{ /* ... */ }
func NewReader(s string) *Reader
~~~
字符串string本身并沒有出現在每個成員名字中。因為用戶會這樣引用這些成員strings.Index、strings.Replacer等。
其它一些包,可能只描述了單一的數據類型,例如html/template和math/rand等,只暴露一個主要的數據結構和與它相關的方法,還有一個以New命名的函數用于創建實例。
~~~
package rand // "math/rand"
type Rand struct{ /* ... */ }
func New(source Source) *Rand
~~~
這可能導致一些名字重復,例如template.Template或rand.Rand,這就是為什么這些種類的包名往往特別短的原因之一。
在另一個極端,還有像net/http包那樣含有非常多的名字和種類不多的數據類型,因為它們都是要執行一個復雜的復合任務。盡管有將近二十種類型和更多的函數,但是包中最重要的成員名字卻是簡單明了的:Get、Post、Handle、Error、Client、Server等。
### 10.7. 工具
本章剩下的部分將討論Go語言工具箱的具體功能,包括如何下載、格式化、構建、測試和安裝Go語言編寫的程序。
Go語言的工具箱集合了一系列的功能的命令集。它可以看作是一個包管理器(類似于Linux中的apt和rpm工具),用于包的查詢、計算的包依賴關系、從遠程版本控制系統和下載它們等任務。它也是一個構建系統,計算文件的依賴關系,然后調用編譯器、匯編器和連接器構建程序,雖然它故意被設計成沒有標準的make命令那么復雜。它也是一個單元測試和基準測試的驅動程序,我們將在第11章討論測試話題。
Go語言工具箱的命令有著類似“瑞士軍刀”的風格,帶著一打子的子命令,有一些我們經常用到,例如get、run、build和fmt等。你可以運行go或go help命令查看內置的幫助文檔,為了查詢方便,我們列出了最常用的命令:
~~~
$ go
...
build compile packages and dependencies
clean remove object files
doc show documentation for package or symbol
env print Go environment information
fmt run gofmt on package sources
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
version print Go version
vet run go tool vet on packages
Use "go help [command]" for more information about a command.
...
~~~
為了達到零配置的設計目標,Go語言的工具箱很多地方都依賴各種約定。例如,根據給定的源文件的名稱,Go語言的工具可以找到源文件對應的包,因為每個目錄只包含了單一的包,并且到的導入路徑和工作區的目錄結構是對應的。給定一個包的導入路徑,Go語言的工具可以找到對應的目錄中沒個實體對應的源文件。它還可以根據導入路徑找到存儲代碼倉庫的遠程服務器的URL。
### 10.7.1. 工作區結構
對于大多數的Go語言用戶,只需要配置一個名叫GOPATH的環境變量,用來指定當前工作目錄即可。當需要切換到不同工作區的時候,只要更新GOPATH就可以了。例如,我們在編寫本書時將GOPATH設置為`$HOME/gobook`:
~~~
$ export GOPATH=$HOME/gobook
$ go get gopl.io/...
~~~
當你用前面介紹的命令下載本書全部的例子源碼之后,你的當前工作區的目錄結構應該是這樣的:
~~~
GOPATH/
src/
gopl.io/
.git/
ch1/
helloworld/
main.go
dup/
main.go
...
golang.org/x/net/
.git/
html/
parse.go
node.go
...
bin/
helloworld
dup
pkg/
darwin_amd64/
...
~~~
GOPATH對應的工作區目錄有三個子目錄。其中src子目錄用于存儲源代碼。每個包被保存在與$GOPATH/src的相對路徑為包導入路徑的子目錄中,例如gopl.io/ch1/helloworld相對應的路徑目錄。我們看到,一個GOPATH工作區的src目錄中可能有多個獨立的版本控制系統,例如gopl.io和golang.org分別對應不同的Git倉庫。其中pkg子目錄用于保存編譯后的包的目標文件,bin子目錄用于保存編譯后的可執行程序,例如helloworld可執行程序。
第二個環境變量GOROOT用來指定Go的安裝目錄,還有它自帶的標準庫包的位置。GOROOT的目錄結構和GOPATH類似,因此存放fmt包的源代碼對應目錄應該為$GOROOT/src/fmt。用戶一般不需要設置GOROOT,默認情況下Go語言安裝工具會將其設置為安裝的目錄路徑。
其中`go env`命令用于查看Go語音工具涉及的所有環境變量的值,包括未設置環境變量的默認值。GOOS環境變量用于指定目標操作系統(例如android、linux、darwin或windows),GOARCH環境變量用于指定處理器的類型,例如amd64、386或arm等。雖然GOPATH環境變量是唯一必需要設置的,但是其它環境變量也會偶爾用到。
~~~
$ go env
GOPATH="/home/gopher/gobook"
GOROOT="/usr/local/go"
GOARCH="amd64"
GOOS="darwin"
...
~~~
### 10.7.2. 下載包
使用Go語言工具箱的go命令,不僅可以根據包導入路徑找到本地工作區的包,甚至可以從互聯網上找到和更新包。
使用命令`go get`可以下載一個單一的包或者用`...`下載整個子目錄里面的每個包。Go語言工具箱的go命令同時計算并下載所依賴的每個包,這也是前一個例子中golang.org/x/net/html自動出現在本地工作區目錄的原因。
一旦`go get`命令下載了包,然后就是安裝包或包對應的可執行的程序。我們將在下一節再關注它的細節,現在只是展示整個下載過程是如何的簡單。第一個命令是獲取golint工具,它用于檢測Go源代碼的編程風格是否有問題。第二個命令是用golint命令對2.6.2節的gopl.io/ch2/popcount包代碼進行編碼風格檢查。它友好地報告了忘記了包的文檔:
~~~
$ go get github.com/golang/lint/golint
$ $GOPATH/bin/golint gopl.io/ch2/popcount
src/gopl.io/ch2/popcount/main.go:1:1:
package comment should be of the form "Package popcount ..."
~~~
`go get`命令支持當前流行的托管網站GitHub、Bitbucket和Launchpad,可以直接向它們的版本控制系統請求代碼。對于其它的網站,你可能需要指定版本控制系統的具體路徑和協議,例如 Git或Mercurial。運行`go help importpath`獲取相關的信息。
`go get`命令獲取的代碼是真實的本地存儲倉庫,而不僅僅只是復制源文件,因此你依然可以使用版本管理工具比較本地代碼的變更或者切換到其它的版本。例如golang.org/x/net包目錄對應一個Git倉庫:
~~~
$ cd $GOPATH/src/golang.org/x/net
$ git remote -v
origin https://go.googlesource.com/net (fetch)
origin https://go.googlesource.com/net (push)
~~~
需要注意的是導入路徑含有的網站域名和本地Git倉庫對應遠程服務地址并不相同,真實的Git地址是go.googlesource.com。這其實是Go語言工具的一個特性,可以讓包用一個自定義的導入路徑,但是真實的代碼卻是由更通用的服務提供,例如googlesource.com或github.com。因為頁面 https://golang.org/x/net/html 包含了如下的元數據,它告訴Go語言的工具當前包真實的Git倉庫托管地址:
~~~
$ go build gopl.io/ch1/fetch
$ ./fetch https://golang.org/x/net/html | grep go-import
<meta name="go-import"
content="golang.org/x/net git https://go.googlesource.com/net">
~~~
如果指定`-u`命令行標志參數,`go get`命令將確保所有的包和依賴的包的版本都是最新的,然后重新編譯和安裝它們。如果不包含該標志參數的話,而且如果包已經在本地存在,那么代碼那么將不會被自動更新。
`go get -u`命令只是簡單地保證每個包是最新版本,如果是第一次下載包則是比較很方便的;但是對于發布程序則可能是不合適的,因為本地程序可能需要對依賴的包做精確的版本依賴管理。通常的解決方案是使用vendor的目錄用于存儲依賴包的固定版本的源代碼,對本地依賴的包的版本更新也是謹慎和持續可控的。在Go1.5之前,一般需要修改包的導入路徑,所以復制后golang.org/x/net/html導入路徑可能會變為gopl.io/vendor/golang.org/x/net/html。最新的Go語言命令已經支持vendor特性,但限于篇幅這里并不討論vendor的具體細節。不過可以通過`go help gopath`命令查看Vendor的幫助文檔。
**練習 10.3:** 從 http://gopl.io/ch1/helloworld?go-get=1 獲取內容,查看本書的代碼的真實托管的網址(`go get`請求HTML頁面時包含了`go-get`參數,以區別普通的瀏覽器請求)。
### 10.7.3. 構建包
`go build`命令編譯命令行參數指定的每個包。如果包是一個庫,則忽略輸出結果;這可以用于檢測包的可以正確編譯的。如果包的名字是main,`go build`將調用連接器在當前目錄創建一個可執行程序;以導入路徑的最后一段作為可執行程序的名字。
因為每個目錄只包含一個包,因此每個對應可執行程序或者叫Unix術語中的命令的包,會要求放到一個獨立的目錄中。這些目錄有時候會放在名叫cmd目錄的子目錄下面,例如用于提供Go文檔服務的golang.org/x/tools/cmd/godoc命令就是放在cmd子目錄(§10.7.4)。
每個包可以由它們的導入路徑指定,就像前面看到的那樣,或者用一個相對目錄的路徑知指定,相對路徑必須以`.`或`..`開頭。如果沒有指定參數,那么默認指定為當前目錄對應的包。 下面的命令用于構建同一個包, 雖然它們的寫法各不相同:
~~~
$ cd $GOPATH/src/gopl.io/ch1/helloworld
$ go build
~~~
或者:
~~~
$ cd anywhere
$ go build gopl.io/ch1/helloworld
~~~
或者:
~~~
$ cd $GOPATH
$ go build ./src/gopl.io/ch1/helloworld
~~~
但不能這樣:
~~~
$ cd $GOPATH
$ go build src/gopl.io/ch1/helloworld
Error: cannot find package "src/gopl.io/ch1/helloworld".
~~~
也可以指定包的源文件列表,這一般這只用于構建一些小程序或做一些臨時性的實驗。如果是main包,將會以第一個Go源文件的基礎文件名作為最終的可執行程序的名字。
~~~
$ cat quoteargs.go
package main
import (
"fmt"
"os"
)
func main() {
fmt.Printf("%q\n", os.Args[1:])
}
$ go build quoteargs.go
$ ./quoteargs one "two three" four\ five
["one" "two three" "four five"]
~~~
特別是對于這類一次性運行的程序,我們希望盡快的構建并運行它。`go run`命令實際上是結合了構建和運行的兩個步驟:
~~~
$ go run quoteargs.go one "two three" four\ five
["one" "two three" "four five"]
~~~
第一行的參數列表中,第一個不是以`.go`結尾的將作為可執行程序的參數運行。
默認情況下,`go build`命令構建指定的包和它依賴的包,然后丟棄除了最后的可執行文件之外所有的中間編譯結果。依賴分析和編譯過程雖然都是很快的,但是隨著項目增加到幾十個包和成千上萬行代碼,依賴關系分析和編譯時間的消耗將變的可觀,有時候可能需要幾秒種,即使這些依賴項沒有改變。
`go install`命令和`go build`命令很相似,但是它會保存每個包的編譯成果,而不是將它們都丟棄。被編譯的包會被保存到*G**O**P**A**T**H*/*p**k**g**目**錄**下*,*目**錄**路**徑**和**s**r**c**目**錄**路**徑**對**應*,*可**執**行**程**序**被**保**存**到*GOPATH/bin目錄。(很多用戶會將$GOPATH/bin添加到可執行程序的搜索列表中。)還有,`go install`命令和`go build`命令都不會重新編譯沒有發生變化的包,這可以使后續構建更快捷。為了方便編譯依賴的包,`go build -i`命令將安裝每個目標所依賴的包。
因為編譯對應不同的操作系統平臺和CPU架構,`go install`命令會將編譯結果安裝到GOOS和GOARCH對應的目錄。例如,在Mac系統,golang.org/x/net/html包將被安裝到$GOPATH/pkg/darwin_amd64目錄下的golang.org/x/net/html.a文件。
針對不同操作系統或CPU的交叉構建也是很簡單的。只需要設置好目標對應的GOOS和GOARCH,然后運行構建命令即可。下面交叉編譯的程序將輸出它在編譯時操作系統和CPU類型:
*gopl.io/ch10/cross*
~~~
func main() {
fmt.Println(runtime.GOOS, runtime.GOARCH)
}
~~~
下面以64位和32位環境分別執行程序:
~~~
$ go build gopl.io/ch10/cross
$ ./cross
darwin amd64
$ GOARCH=386 go build gopl.io/ch10/cross
$ ./cross
darwin 386
~~~
有些包可能需要針對不同平臺和處理器類型使用不同版本的代碼文件,以便于處理底層的可移植性問題或提供為一些特定代碼提供優化。如果一個文件名包含了一個操作系統或處理器類型名字,例如net_linux.go或asm_amd64.s,Go語言的構建工具將只在對應的平臺編譯這些文件。還有一個特別的構建注釋注釋可以提供更多的構建過程控制。例如,文件中可能包含下面的注釋:
~~~
// +build linux darwin
~~~
在包聲明和包注釋的前面,該構建注釋參數告訴`go build`只在編譯程序對應的目標操作系統是Linux或Mac OS X時才編譯這個文件。下面的構建注釋則表示不編譯這個文件:
~~~
// +build ignore
~~~
更多細節,可以參考go/build包的構建約束部分的文檔。
~~~
$ go doc go/build
~~~
### 10.7.4. 包文檔
Go語言的編碼風格鼓勵為每個包提供良好的文檔。包中每個導出的成員和包聲明前都應該包含目的和用法說明的注釋。
Go語言中包文檔注釋一般是完整的句子,第一行是包的摘要說明,注釋后僅跟著包聲明語句。注釋中函數的參數或其它的標識符并不需要額外的引號或其它標記注明。例如,下面是fmt.Fprintf的文檔注釋。
~~~
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
~~~
Fprintf函數格式化的細節在fmt包文檔中描述。如果注釋后僅跟著包聲明語句,那注釋對應整個包的文檔。包文檔對應的注釋只能有一個(譯注:其實可以有多個,它們會組合成一個包文檔注釋),包注釋可以出現在任何一個源文件中。如果包的注釋內容比較長,一般會放到一個獨立的源文件中;fmt包注釋就有300行之多。這個專門用于保存包文檔的源文件通常叫doc.go。
好的文檔并不需要面面俱到,文檔本身應該是簡潔但可不忽略的。事實上,Go語言的風格更喜歡簡潔的文檔,并且文檔也是需要像代碼一樣維護的。對于一組聲明語句,可以用一個精煉的句子描述,如果是顯而易見的功能則并不需要注釋。
在本書中,只要空間允許,我們之前很多包聲明都包含了注釋文檔,但你可以從標準庫中發現很多更好的例子。有兩個工具可以幫到你。
首先是`go doc`命令,該命令打印包的聲明和每個成員的文檔注釋,下面是整個包的文檔:
~~~
$ go doc time
package time // import "time"
Package time provides functionality for measuring and displaying time.
const Nanosecond Duration = 1 ...
func After(d Duration) <-chan Time
func Sleep(d Duration)
func Since(t Time) Duration
func Now() Time
type Duration int64
type Time struct { ... }
...many more...
~~~
或者是某個具體包成員的注釋文檔:
~~~
$ go doc time.Since
func Since(t Time) Duration
Since returns the time elapsed since t.
It is shorthand for time.Now().Sub(t).
~~~
或者是某個具體包的一個方法的注釋文檔:
~~~
$ go doc time.Duration.Seconds
func (d Duration) Seconds() float64
Seconds returns the duration as a floating-point number of seconds.
~~~
該命令并不需要輸入完整的包導入路徑或正確的大小寫。下面的命令將打印encoding/json包的`(*json.Decoder).Decode`方法的文檔:
~~~
$ go doc json.decode
func (dec *Decoder) Decode(v interface{}) error
Decode reads the next JSON-encoded value from its input and stores
it in the value pointed to by v.
~~~
第二個工具,名字也叫godoc,它提供可以相互交叉引用的HTML頁面,但是包含和`go doc`命令相同以及更多的信息。10.1節演示了time包的文檔,11.6節將看到godoc演示可以交互的示例程序。godoc的在線服務 https://godoc.org ,包含了成千上萬的開源包的檢索工具。
你也可以在自己的工作區目錄運行godoc服務。運行下面的命令,然后在瀏覽器查看 http://localhost:8000/pkg 頁面:
~~~
$ godoc -http :8000
~~~
其中`-analysis=type`和`-analysis=pointer`命令行標志參數用于打開文檔和代碼中關于靜態分析的結果。
### 10.7.5. 內部包
在Go語音程序中,包的封裝機制是一個重要的特性。沒有導出的標識符只在同一個包內部可以訪問,而導出的標識符則是面向全宇宙都是可見的。
有時候,一個中間的狀態可能也是有用的,對于一小部分信任的包是可見的,但并不是對所有調用者都可見。例如,當我們計劃將一個大的包拆分為很多小的更容易維護的子包,但是我們并不想將內部的子包結構也完全暴露出去。同時,我們可能還希望在內部子包之間共享一些通用的處理包,或者我們只是想實驗一個新包的還并不穩定的接口,暫時只暴露給一些受限制的用戶使用。

為了滿足這些需求,Go語言的構建工具對包含internal名字的路徑段的包導入路徑做了特殊處理。這種包叫internal包,一個internal包只能被和internal目錄有同一個父目錄的包所導入。例如,net/http/internal/chunked內部包只能被net/http/httputil或net/http包導入,但是不能被net/url包導入。不過net/url包卻可以導入net/http/httputil包。
~~~
net/http
net/http/internal/chunked
net/http/httputil
net/url
~~~
### 10.7.6. 查詢包
`go list`命令可以查詢可用包的信息。其最簡單的形式,可以測試包是否在工作區并打印它的導入路徑:
~~~
$ go list github.com/go-sql-driver/mysql
github.com/go-sql-driver/mysql
~~~
`go list`命令的參數還可以用`"..."`表示匹配任意的包的導入路徑。我們可以用它來列表工作區中的所有包:
~~~
$ go list ...
archive/tar
archive/zip
bufio
bytes
cmd/addr2line
cmd/api
...many more...
~~~
或者是特定子目錄下的所有包:
~~~
$ go list gopl.io/ch3/...
gopl.io/ch3/basename1
gopl.io/ch3/basename2
gopl.io/ch3/comma
gopl.io/ch3/mandelbrot
gopl.io/ch3/netflag
gopl.io/ch3/printints
gopl.io/ch3/surface
~~~
或者是和某個主題相關的所有包:
~~~
$ go list ...xml...
encoding/xml
gopl.io/ch7/xmlselect
~~~
`go list`命令還可以獲取每個包完整的元信息,而不僅僅只是導入路徑,這些元信息可以以不同格式提供給用戶。其中`-json`命令行參數表示用JSON格式打印每個包的元信息。
~~~
$ go list -json hash
{
"Dir": "/home/gopher/go/src/hash",
"ImportPath": "hash",
"Name": "hash",
"Doc": "Package hash provides interfaces for hash functions.",
"Target": "/home/gopher/go/pkg/darwin_amd64/hash.a",
"Goroot": true,
"Standard": true,
"Root": "/home/gopher/go",
"GoFiles": [
"hash.go"
],
"Imports": [
"io"
],
"Deps": [
"errors",
"io",
"runtime",
"sync",
"sync/atomic",
"unsafe"
]
}
~~~
命令行參數`-f`則允許用戶使用text/template包(§4.6)的模板語言定義輸出文本的格式。下面的命令將打印strconv包的依賴的包,然后用join模板函數將結果鏈接為一行,連接時每個結果之間用一個空格分隔:
{% raw %}
~~~
$ go list -f '{{join .Deps " "}}' strconv
errors math runtime unicode/utf8 unsafe
~~~
{% endraw %}
譯注:上面的命令在Windows的命令行運行會遇到`template: main:1: unclosed action`的錯誤。產生這個錯誤的原因是因為命令行對命令中的`" "`參數進行了轉義處理。可以按照下面的方法解決轉義字符串的問題:
{% raw %}
~~~
$ go list -f "{{join .Deps \" \"}}" strconv
~~~
{% endraw %}
下面的命令打印compress子目錄下所有包的依賴包列表:
{% raw %}
~~~
$ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/...
compress/bzip2 -> bufio io sort
compress/flate -> bufio fmt io math sort strconv
compress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time
compress/lzw -> bufio errors fmt io
compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io
~~~
{% endraw %}
譯注:Windows下有同樣有問題,要避免轉義字符串的干擾:
{% raw %}
~~~
$ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/...
~~~
{% endraw %}
`go list`命令對于一次性的交互式查詢或自動化構建或測試腳本都很有幫助。我們將在11.2.4節中再次使用它。每個子命令的更多信息,包括可設置的字段和意義,可以用`go help list`命令查看。
在本章,我們解釋了Go語言工具中除了測試命令之外的所有重要的子命令。在下一章,我們將看到如何用`go test`命令去運行Go語言程序中的測試代碼。
**練習 10.4:** 創建一個工具,根據命令行指定的參數,報告工作區所有依賴指定包的其它包集合。提示:你需要運行`go list`命令兩次,一次用于初始化包,一次用于所有包。你可能需要用encoding/json(§4.5)包來分析輸出的JSON格式的信息。
- Go語言程序設計
- Go語言圣經(中文版)
- 譯者序
- 前言
- 第一章 入門
- 第二章 程序結構
- 第三章 基礎數據類型
- 第四章 復合數據類型
- 第五章 函數
- 第六章 方法
- 第七章 接口
- 7.4. flag.Value接口
- 7.6. sort.Interface接口
- 7.7. http.Handler接口
- 第八章 Goroutines和Channels
- 第九章 基于共享變量的并發
- 9.2. sync.Mutex互斥鎖
- 第十章 包和工具
- 第十一章 測試
- 第十二章 反射
- 12.2. reflect.Type和reflect.Value
- 12.5. 通過reflect.Value修改值
- 第13章 底層編程
- 13.1. unsafe.Sizeof, Alignof 和 Offsetof
- 附錄