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命令很相似,但是它會保存每個包的編譯成果,而不是將它們都丟棄。被編譯的包會被保存到$GOPATH/pkg目録下,目録路徑和 src目録路徑對應,可執行程序被保存到$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模闆函數將結果鏈接爲一行,連接時每個結果之間用一個空格分隔:

$ go list -f '{{join .Deps " "}}' strconv
errors math runtime unicode/utf8 unsafe

譯註:上面的命令在Windows的命令行運行會遇到template: main:1: unclosed action的錯誤。産生這個錯誤的原因是因爲命令行對命令中的" "參數進行了轉義處理。可以按照下面的方法解決轉義字符串的問題:

$ go list -f "{{join .Deps \" \"}}" strconv

下面的命令打印compress子目録下所有包的依賴包列表:

$ 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

譯註:Windows下有同樣有問題,要避免轉義字符串的榦擾:

$ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/...

go list命令對於一次性的交互式査詢或自動化構建或測試腳本都很有幫助。我們將在11.2.4節中再次使用它。每個子命令的更多信息,包括可設置的字段和意義,可以用go help list命令査看。

在本章,我們解釋了Go語言工具中除了測試命令之外的所有重要的子命令。在下一章,我們將看到如何用go test命令去運行Go語言程序中的測試代碼。

練習 10.4: 創建一個工具,根據命令行指定的參數,報告工作區所有依賴指定包的其它包集合。提示:你需要運行go list命令兩次,一次用於初始化包,一次用於所有包。你可能需要用encoding/json(§4.5)包來分析輸出的JSON格式的信息。