《Go 語言程式設計》讀書筆記 (九) 命令工具集

KevinYan發表於2020-01-14

Go語言的工具箱集合了一系列的功能的命令集。它可以看作是一個包管理器(類似於Linux中的apt和rpm工具),用於完成包的查詢、計算的包依賴關係、從遠端版本控制系統和下載它們等任務。它也是一個構建系統,計算檔案的依賴關係,然後呼叫編譯器、彙編器和聯結器構建程式。它被設計成沒有標準的make命令那麼複雜。它也是一個單元測試和基準測試的驅動程式。

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版本下執行會有不同的輸出,尤其是在 1.11版本後會有 mod 子命令,我們不會涉及go mod這個話題。

下載包

使用Go語言工具箱的go命令,不僅可以根據包匯入路徑找到本地工作區的包,甚至可以從網際網路上找到和更新包。

使用命令go get可以下載一個單一的包或者用...下載整個子目錄裡面的每個包。go命令同時計算並下載所依賴的每個包。一旦go get命令下載了包,後面就是安裝包或包對應的可執行的程式。

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)

需要注意的是golang.org/x/net包的匯入路徑含有的網站域名和本地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命令只是簡單地保證每個包是最新版本,如果是第一次下載包則是比較很方便的;但是對於已經發布的程式則可能是不合適的,因為程式可能需要對依賴的包做精確的版本依賴管理。

構建包

go build命令編譯命令列引數指定的每個包。如果包是一個庫,則忽略輸出結果;這可以用於檢測包是否可以被正確編譯。如果包的名字是main,go build將呼叫聯結器在當前目錄建立一個可執行程式;以匯入路徑的最後一段作為可執行程式的名字。

因為每個目錄只包含一個包,因此每個對應可執行程式的包,會要求放到一個獨立的目錄中。這些目錄有時候會放在名叫cmd目錄的子目錄下面,例如用於提供Go文件服務的golang.org/x/tools/cmd/godoc命令就是放在cmd子目錄。

每個包可以由它們的匯入路徑指定,就像前面看到的那樣,或者用一個相對目錄的路徑指定,相對路徑必須以...開頭。如果沒有指定引數,那麼預設指定為當前目錄對應的包。 下面的命令用於構建同一個包, 雖然它們的寫法各不相同:

$ 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型別:

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

包文件

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.

第二個工具,名字也叫godoc,它提供可以相互交叉引用的HTML頁面,但是包含和go doc命令相同以及更多的資訊。godoc的線上服務 https://godoc.org ,包含了成千上萬的開源包的檢索工具。

你也可以在自己的工作區目錄執行godoc服務。執行下面的命令,然後在瀏覽器檢視 http://localhost:8000/pkg 頁面:

$ godoc -http :8000

內部包

在Go語言程式中,包的封裝機制是一個重要的特性。沒有匯出的識別符號只在同一個包內部可以訪問,而匯出的識別符號則是面向全宇宙都是可見的。有時候,一箇中間的狀態可能也是有用的,對於一小部分信任的包是可見的,但並不是對所有呼叫者都可見。例如,當我們計劃將一個大的包拆分為很多小的更容易維護的子包,但是我們並不想將內部的子包結構也完全暴露出去。同時,我們可能還希望在內部子包之間共享一些通用的功能。

為了滿足這些需求,Go語言的構建工具對匯入路徑包含internal的包做了特殊處理。這種包叫internal包,一個internal包只能被和internal目錄有同一個父目錄的包所匯入。例如,net/http/internal/chunked內部包只能被net/http/httputilnet/http包匯入,但是不能被net/url包匯入。不過net/url包可以匯入net/http/httputil包。

net/http
net/http/internal/chunked
net/http/httputil
net/url

本作品採用《CC 協議》,轉載必須註明作者和本文連結

公眾號:網管叨bi叨 | Golang、PHP、Laravel、Docker等學習經驗分享

相關文章