讀古今文學網 > Go語言程序設計 > 第9章 包 >

第9章 包

Go語言的標準庫裡包含大量的包,提供了大量、廣泛而且富有創意的各種功能。另外,在Go Dashboard(godashboard.appspot.com/project)上還有很多第三方的包可以使用。

除了這些,我們還能創建自己的包,安裝到標準庫裡面,或者只是保留在我們自己的Go語言目錄樹裡,也就是GOPATH路徑。

在這一章我們將描述如何創建和導入一個自定義的包或者第三方的包,然後簡略地瞭解下gc編譯器的一些命令行參數,最後,我們來看一下Go語言的標準庫,避免重複造輪子。

9.1 自定義包

到目前為止,我們見過的所有例子都是以一個包的形式存在的,也就是 main 包。在 Go語言裡,允許我們將同一個包的代碼分隔成多個小塊來單獨保存,只需要將這些文件放在同一個目錄即可。例如,第8章的invoicedata例子,雖然有6個獨立的文件(invoicedata.go、gob.go、inv.go、jsn.go、txt.go和xml.go),但是每個文件的第一條語句都是包(main),表明它們都是同屬於一個包的,也就是main包。

對於更大的應用程序,我們可能更喜歡將它的功能性分隔成邏輯的單元,分別在不同的包裡實現。或者將一些應用程序通用的那一部分剖離出來。Go語言裡並沒有限制一個應用程序能導入多少個包或者一個包能被多少個應用程序共享,但是將這些應用程序特定的包放在當前應用程序的子目錄下和放在GOPATH源碼目錄下是不大一樣的。我們所說的GOPATH源碼目錄是一個叫src的目錄,每一個在GOPATH環境變量裡的的目錄都應該包含這個目錄,因為Go語言的工具鏈就是要求這樣做的。我們的程序和包都應該在這個src目錄下的子目錄裡。

我們也可以將我們自己的包安裝到Go語言包目錄樹下,也就是GOROOT下,但是這樣沒有什麼好處而且可能會不太方便,因為有些系統是通過包管理系統來安裝 Go語言的,有些是通過安裝包,有些是手動編譯的。

9.1.1 創建自定義的包

我們創建的自定義的包最好就放在GOPATH的src目錄下(或者GOPATH src的某個子目錄),如果這個包只屬於某個應用程序,可以直接放在應用程序的子目錄下,但如果我們希望這個包可以被其他的應用程序共享,那就應該放在GOPATH的src目錄下,每個包單獨放在一個目錄裡,如果兩個不同的包放在同一個目錄下,會出現名字衝突的編譯錯誤。

作為慣例,包的源代碼應放在一個同名的文件夾下面。同一個包可以有任意多個文件,文件的名字也沒有任何規定(但後續名必須是.go),在這本書裡我們假設包名就是.go的文件名(如果一個包有多個.go文件,則其中會有一個.go文件的文件名和包名相同)。

第1章(1.5節)的stacker例子由一個主程序(在stacker.go文件裡)和一個自定義的stack包(在文件stack.go裡)組成,源碼目錄的層次結構如下:

aGoPath/src/stacker/stacker.go

aGoPath/src/stacker/stack/stack.go

GOPATH環境變量是由多個目錄路徑組成且路徑之間以冒號(Windows上是分號)分隔開的字符串,這裡的aGoPath就是GOPATH路徑集合中的其中一個路徑。

我們在stacker目錄裡執行go build命令,就會得到一個stacker的可執行文件(在Windows 系統上是 stacker.exe)。但是,如果我們希望生成的可執行文件放到 GOPATH的bin 目錄裡,或者想將 stacker/stack 包共享給其他的應用程序使用,這就必須使用 go install來完成。

當執行go install命令創建stacker程序時,會創建兩個目錄(如果不存在就會創建):aGoPath/bin和aGoPath/pkg/linux_amd64/stacker,前者包含了stacker可執行文件,後者包含了stack包的靜態庫文件(至於linux_amd64等會根據不同的系統和硬件體系結構而變化,例如在32位的Windows系統上是windows_386)。

需要在 stacker 程序中使用 stack 包時,在程序源文件中使用導入語句 import〞stacker/stack〞即可,也就是絕對路徑(Unix風格)去除aGoPath/src這部分。事實上,只要這個包放在GOPATH下,都可以被別的程序或者包導入,GOPATH下的包沒有共享和專用之分。

又比如第6章(6.5.3節)實現的有序映射是在omap包裡,它被設計為可由多個程序使用。為了避免包名的衝突,我們在GOPATH(如果GOPATH有多個路徑,任意一個路徑都可以)路徑下創建了一個具有唯一名字(這裡用了域名)的目錄,結構如下:

aGoPath/src/qtrac.eu/omap/omap.go

這樣其他的程序,只要它們也在某個 GOPATH 目錄下面,都可以通過使用 import〞qtrac.eu/omap〞來導入這個包。如果我們還有其他的包需要共享,則將它們放到aGoPath/src/qtrac.eu路徑下即可。

當使用 go install 安裝 omap 包的時候,它創建了 aGoPath/pkg/linux_amd64/qtrac.eu目錄(如果不存在的話),保存了omap包的靜態庫文件,其中linux_amd64是根據不同的系統和硬件體系結構而變化的。

如果我們希望在一個包裡創建新的包,例如,在my_package 包下面創建兩個新的包pkg1和pkg2,可以這麼做:在aGoPath/src/my_package下創建兩個子目錄,例如aGoPath/src/my_package/pkg1和aGoPath/src/my_package/pkg2,對應的包文件是 aGoPath/src/my_package/pkg1/pkg1.go和aGoPath/src/my_package/pkg2/pkg2.go。之後,假如想導入pkg2,使用import my_package/pkg2即可。Go語言標準庫的源碼樹就是這樣的結構。當然,my_package 目錄可以有它自己的包,如 aGoPath/src/my_package/my_package.go文件。

Go語言中的包導入的搜索路徑是首先到GOROOT(即$GOROOT/pkg/${GOOS}_${GOARCH},比如/opt/go/pkg/linux_amd64),然後是GOPATH 環境變量下的目錄。這就意味這可能會有名字衝突。最簡單的方法就是確保GOPATH裡包含的每個路徑都是唯一的,例如之前我們以域名來作為omap的包的目錄。

在Go程序裡使用標準庫裡的包和使用GOPATH路徑下的包是一樣的,下面幾個小節我們來討論一些平台特定的代碼。

9.1.1.1 平台特定的代碼

在某些情況下,我們必須為不同的平台編寫一些特定的代碼。例如,在類Unix的系統上,通常shell都支持通配符(也叫globbing),所以在命令行輸入*.txt,程序就能夠從os.Args[1:]切片裡讀取到比如[〞README.txt〞,〞INSTALL.txt〞]這些值。但是在Windows平台上,程序只會接收到[〞*.txt〞),我們可以使用filepath.Glob函數來實現通配符的功能,但是這只需要在Windows平台上使用。

那如何決定什麼時候才需要使用filepath.Glob函數呢,使用if runtime.GOOS ==〞windows〞 {...}即可,這也是本書中使用最廣的方法,例如cgrep1/cgrep1.go程序等等。另一種辦法就是使用平台特定的代碼來實現,例如,cgrep3程序有3個文件,cgrep.go、util_linux.go、util_windows.go,其中util_linux.go定義了這麼一個函數:

func commandLineFiles(files string) string { return files }

很明顯,這個函數並沒有處理文件名通配,因為在類 Unix 系統上沒必要這麼做。而util_windows.go文件則定義了另一個同名的函數。

func commandLineFiles(files string) string {

args := make(string, 0, len(files))

for _, name := range files {

if matches, err := filepath.Glob(name); err != nil {

args = append(args, name) // 無效模式

} else if matches != nil { // 至少有一個匹配

args = append(args, matches...)

}

}

return args

}

當我們使用go build來創建cgrep3程序時,在Linux機器上util_linux.go文件會被編譯而 util_windows.go 則被忽略,而在Windows平台恰好相反,這樣就確保了只有一個commandLineFiles函數被實際編譯了。

在 Mac OS X 系統和FreeBSD 系統上,既不會編譯 util_linux.go 也不會編譯util_windows.go,所以go build會返回失敗。但是我們可以創建一個軟鏈接或者直接複製util_linux.go到util_darwin.go或者util_freebsd.go,因為這兩個平台的shell也是支持通配符的,這樣就能正常構建Mac OS X和FreeBSD平台的程序了。

9.1.1.2 文檔化相關的包

如果我們想共享一些包,為了方便其他的開發者使用,需要編寫足夠的文檔才行。Go語言提供了非常方便的文檔化工具 godoc,可以在命令行顯示包的文檔和函數,也可以作為一個Web服務器啟動,如圖9-1所示[1]。godoc會自動搜索GOPATH路徑下的所有包並將它顯示出來,如果某些包不在GOPATH路徑下,可以使用-path參數(除此之外還要有-http參數)來指定(我們在關於Go語言官方文檔的部分討論了godoc,參見1.1節)。

圖9-1 omap包的文檔

好的文檔應該怎麼寫,這是一個一直爭論不休的問題,因此在這一節我們只是純粹地來瞭解一下Go語言的文檔化機制。

在默認情況下,只有可導出的類型、類、常量和變量才會在godoc裡出現,因此全部這些內容應該添加合適的文檔。文檔都是直接包含在源文件裡。這裡以omap包為例(omap包我們之前已經在第6章講過了)。

// Package omap implements an efficient key-ordered map.

//

// Keys and values may be of any type, but all keys must be comparable

// using the less than function that is passed in to the omap.New

// function, or the less than function provided by the omap.New*

// construction functions.

package omap

對於一個包來說,在包聲明語句(package)之前的註釋被視為是對包的說明,第一句是一行簡短的描述,通常以句號結束,如果沒有句號,則以換行符號結束。

// Map is a key-ordered map.

// The zero value is an invalid map! Use one of the construction functions

// (e.g., New), to create a map for a specific key type.

type Map struct {

可導出類型聲明的文檔必須緊接在該類型聲明之前,而且必須總是描述該類型的零值是否有效。

// New returns an empty Map that uses the given less than function to

// compare keys.For example:

//  type Point { X, Y int }

//    pointMap := omap.New(func(a, b interface{}) bool {

//      α, β := a.(Point), b.(Point)

//      ifα.X != β.X {

//        returnα.X < β.X

//      }

//      returnα.Y < β.Y

//    })

func New(less func(interface{}, interface{}) bool) *Map {

函數或者方法的文檔必須緊接在它們的第一行代碼之前。上面這個例子是對 omap 包的New構造函數的註釋。

圖9-2以Web的方式展示了一個函數的文檔是什麼樣的,同時註釋裡縮進的文本會被視為代碼顯示在HTML頁面上。但是在我寫這本書的時候,godoc還不支持任何標記,例如bold、italic、links等。

// NewCaseFoldedKeyed returns an empty Map that accepts case-insensitive

// string keys.

func NewCaseFoldedKeyed *Map {

圖9-2 omap包中New函數的文檔

上面這段文檔是描述了一個出於便捷方面考慮而提供的輔助構造函數,基於一個預定義的比較函數。

// Insert inserts a new key-value into the Map and returns true; or

// replaces an existing key-value pair's value if the keys are equal and

// returns false.For example:

//  inserted := myMap.Insert(key, value).

func (m *Map) Insert(key, value interface{}) (inserted bool) {

這段是Insert方法的文檔。可以注意到Go語言裡的文檔描述通常是以函數或者方法的名字開頭的,這是一個慣例,還有(這個不是慣例)就是在文檔中引用函數或者方法時不使用圓括號。

9.1.1.3 包的單元測試和基準測試相關的包

Go語言標準庫的testing包對單元測試提供了很好的支持。對一個包進行單元測試是一件很簡單的事情,只需要在這個包的根目錄下創建一個測試文件即可。單元測試的文件名格式為包名_test.go。例如,omap包的單元測試文件為omap_test.go。

在我們這本書裡,單元測試文件都是放在一個獨立的包(如 omap_test)下面,然後導入需要被測試的包、testing包以及其他一些測試依賴的包。這實際上限制我們只能進行黑盒測試。但是也有些 Go語言程序員會傾向於白盒測試。這很容易做到,只需將測試文件和包源代碼放在一起即可,這種情況下我們不需要導入被測試的包,而且還可以測試那些非導出的數據類型,甚至為它們增加一些新的方法以方便測試。

單元測試文件比較特殊的一點是,它沒有main函數。取而代之的是,它有一些以Test開頭的函數,並且必須只有一個*testing.T類型的參數,沒有返回值。我們還可以增加任意其他的輔助函數,當然,這些函數不能以Test開頭。

func TestStringKeyOMapInsertion(t *testing.T) {

wordForWord := omap.NewCaseFoldedKeyed

for _, word := range string{〞one〞, 〞Two〞, 〞THREE〞, 〞four〞, 〞Five〞} {

wordForWord.Insert(word, word)

}

var words string

wordForWord.Do(func(_, value interface{}) {

words = append(words, value.(string))

})

actual, expected := strings.Join(words, 〞〞), 〞FivefouroneTHREETwo〞

if actual != expected {

t.Errorf(〞%q != %q〞, actual, expected)

}

}

這是 omap_test.go 文件裡的一個單元測試。首先創建一個空的omap.Map,然後插入一些字符串類型的鍵和值(注意鍵名是區分大小寫的)。然後使用Map.Do方法遍歷,將得到的每個值追加到一個字符串切片裡去。最後,將這個字符串切片組合成一個字符串,檢查結構是否是我們所期望的。如果結果不對,則調用testing.T.Errorf方法報告詳細的失敗的原因。如果錯誤或者失敗方法沒有被調用,我們就可以假定測試已經通過了。

測試通過的結果類似如下。

$ go test

ok    qtrac.eu/omap

PASS

如果測試的時候使用 -test.v選項,則會輸出更詳細的信息。

$ go test -test.v

ok     qtrac.eu/omap

=== RUN TestStringKeyOMapInsertion-4

--- PASS: TestStringKeyOMapInsertion-4 (0.00 seconds)

=== RUN TestIntKeyOMapFind-4

--- PASS: TestIntKeyOMapFind-4 (0.00 seconds)

=== RUN TestIntKeyOMapDelete-4

--- PASS: TestIntKeyOMapDelete-4 (0.00 seconds)

=== RUN TestPassing-4

--- PASS: TestPassing-4 (0.00 seconds)

PASS

如果測試不通過,會得到如下信息(這裡我們人為地修改了常量的字符串值,強制它失敗),如果指定了-test.v選項,得到的信息可能更多。

$ go test

FAIL      qtrac.eu/omap

--- FAIL: TestStringKeyOMapInsertion-4 (0.01 seconds)

omap_test.go:35: 〞FivefouroneTHREETwo〞 != 〞FivefouroneTHREEToo〞

FAIL

另外,這個例子裡用到了Errorf方法,testing包的*testing.T還有很多其他的方法可以使用,如testing.T.Fail、testing.T.Fatal等。利用這些方法我們可以實現測試的調試級別。

此外,testing 包還支持基準測試,和其他的測試函數一樣,基準測試也是放在package_test.go文件裡的,唯一不同的就是基準測試的函數名必須以Benchmark開頭,並且必須有一個*testing.B類型的參數,沒有返回值。

func BenchmarkOMapFindSuccess(b *testing.B) {

b.StopTimer // Don't time creation and population

intMap := omap.NewIntKeyed

for i := 0; i < 1e6; i++ {

intMap.Insert(i, i)

}

b.StartTimer // Time the Find method succeeding

for i := 0; i < b.N; i++ {

intMap.Find(i % 1e6)

}

}

函數一開始是就執行 b.StopTimer來停止計時器,因為我們不希望將創建和生成omap.Map的時間也計算在內。我們創建一個空的omap.Map,然後插入一百萬條記錄。

默認情況下 go test 不會執行基準測試,所以如果我們需要基準測試的時候必須指定-test.bench 選項,還需要有一個正則表達式字符串,來匹配我們需要執行的基準測試函數名。例如.*表示所有的基準測試函數都會被執行(只有一個.也行)。

$ go test -test.bench=.

PASS     qtrac.eu/omap

PASS

BenchmarkOMapFindSuccess-4  1000000  1380ns/op

BenchmarkOMapFindFailure-4  1000000  1350ns/op

從這個結果我們可以看出,兩個函數都遍歷了一百萬次,還給出了每次操作的時間消耗。至於遍歷多少次是由go test來決定的,也就是b.N,不過我們可以使用-test.benchtime選項來指定我們希望每個基準測試的執行時間為多少秒。

本書中還有其他的一些例子,也是使用package_test.go作為測試文件的。

9.1.2 導入包

Go語言允許我們對導入的包使用別名來標識。這個特性是非常方便和有用的,例如,可以在不同的實現之間進行自由的切換。舉個例子,假如我們實現了bio包的兩個版本bio_v1和bio_v2,現在在某個程序裡使用了import bio 〞bio_v1〞,如果需要切換到另一個版本的實現,只需要將bio_v1改成bio_v2即可,即import bio 〞bio_v2〞,但是需要注意的是,bio_v1和bio_v2的API必須是相同的,或者bio_v2是bio_v1的超集,這樣其餘所有的的代碼都不需要做任何改動。另外,最好就不要對官方標準庫的包使用別名,因為這樣可能會導致一些混淆或激怒後來的維護者。

我們之前在第5章提到過(見5.6.2節),當導入一個包時,它所有的init函數就會被執行。有些時候我們並非真的需要使用這些包,僅僅是希望它的init函數被執行而已。

舉個例子,如果我們需要處理圖像,通常會導入 Go 標準庫支持的所有相關的包,但是並不會用到這些包的任何函數。下面就是imagetag1.go程序的導入語句部分。

import (

〞fmt〞

〞image〞

〞os〞

〞path/filepath〞

〞runtime〞

_ 〞image/gif〞

_ 〞image/jpeg〞

_ 〞image/png〞

)

這裡導入了image/gif、image/jpeg和image/png包,純粹是為了讓它們的init函數被執行(這些init函數註冊了各自的圖像格式),所有這些包都以下劃線作為別名,所以Go語言不會發出導入了某個包但是沒有使用的警告。

9.2 第三方包

Go語言的工具鏈幾乎貫穿全書了,我們使用它來創建程序和包,如omap包等。除此之外,我們還可以用來下載、編譯和安裝第三方的包。當然,前提必須是我們的計算機能夠連接網絡。godashboard.appspot.com/project上面維護了一系列第三方的包。(另外一種方法就是通過下載源碼,通常是通過版本控制系統來下載,然後本地編譯。)

需要安裝Go Dashboard的包的話,首先點擊它的鏈接到包的主頁,然後找到有go get命令的地方,通常那就是介紹如何下載和安裝包的了。

舉個例子,我們點擊Go Dashboard頁面上的freetype-go.googlecode.com/hg/ freetype鏈接,然後它會將我們帶到code.google.com/p/freetype-go/主頁,這個頁面上有如何安裝的相關介紹,在我寫這本書的時候,這個命令是go get freetype-go.google- code.com/hg/freetype。

畢竟這個包是來自於第三方的,go get 還必須將它安裝到我們計算機上的某個地方。默認情況下會安裝到GOPATH環境變量的第一個路徑,如果沒法將這個包保存到那裡,就自動安裝到GOROOT目錄。如果我們想強制go get默認使用GOROOT目錄,可以在go get運行之前清空GOPATH環境變量中的路徑集合。

執行go get 之後,就自動開始下載、創建和安裝包了。如果想瞭解最新安裝的包的文檔,可以以Web服務的方式來運行godoc,例如godoc -http=:8000,這樣就可以查看這個包的文檔了。

為了避免名字上的衝突,第三方的包通常使用域名來確保唯一性。舉個例子,假如我們想使用FreeType這個包的話,可以這樣導入:

import 〞freetype-go.googlecode.com/hg/freetype〞

當然,使用這個包的函數我們只需要最後一部分即可,也就是freetype,比如font, err :=freetype.ParseFont(fontdata)。如果很不幸地連最後一部分也產生名字衝突了,我們還可以使用別名,例如,import ftype 〞freetype-go.googlecode.com/hg/freetype〞,然後在我們的代碼裡這樣寫font, err := ftype.ParseFont(fontdata)。

第三方的包通常都可以在Go 1上使用,但是有些需要更新的Go版本,或者提供多個版本的下載。比如,有一些需要在最新的開發版上才能使用。通常情況下,最好只使用穩定版的Go (目前是Go 1)和與該版本兼容的第三方包。

9.3 Go命令行工具簡介

安裝Go的gc編譯器自然也就包括了編譯器和連接器(6g、6l等),還有其他的一些工具。最常用的就是go,既可以用來創建我們自己的程序和包,又可以下載和安裝第三方的程序和包,還可以用來執行單元測試和基準測試,如我們之前在9.1.1.3節中見到的一樣。如果需要更多的幫助可以使用go help命令,go help會顯示一個命令列表,當然文檔化的godoc工具也在其中。

除了我們這本書所用到的工具,還有其他的一些工具和go tool命令,這裡我們會介紹一些。其中一個就是go vet命令,它可以檢查Go程序的一些簡單錯誤,特別是fmt包的打印函數。

另一個命令就是 go fix,有時候 Go語言的新發行版會包含一些語言上的變更,或者更多的是標準庫API的修改,這樣會導致我們寫好的代碼編譯不過。這種情況下可以在我們代碼的根目錄下執行go fix命令來進行自動的升級。我們強烈推薦你使用版本控制系統來管理你的.go 文件,這樣所有的修改都會記錄下來,或者在運行 go fix之前至少也做一個備份。這樣做的原因是go fix有可能會破壞我們現有的代碼,如果真的發生了,至少我們還可以恢復它。我們還可以使用帶有-diff選項的go fix命令,這樣可以看到go fix將要修改哪些地方,但並不會真地修改它們。

最後一個要介紹的命令就是gofmt。它能以一種標準化的方式來格式化Go代碼,這也是Go的開發者強烈推薦使用的。使用gofmt的最大好處就是,你不需要考慮哪種編排方式最好, gofmt 可以讓你所有的代碼看起來都是同一種風格。我們這本書所有的代碼都是經過 gofmt格式化的,不過超過75個字符的代碼行會被自動折行以適合本書的頁寬。

9.4 Go標準庫簡介

Go標準庫裡包含大量的包,功能非常豐富。我們這裡只是做一個很簡單的介紹,因為標準庫的內容是隨時都有可能有改動的,最好的方式就是瀏覽官方在線版的標準庫(golang.org/pkg/),或者使用本地的godoc,這兩種方式都能看到最新的信息並且可以更好地理解每個包提供了什麼樣的功能。

其中 exp(experimental)包包含一些將來有可能(也可能不)被增加到標準庫裡面去的實驗性包,所以除非我們想參與標準庫的開發,否則就不要使用這個包。在以編譯源代碼方式安裝Go的時候通常會帶有這個exp包,但通常不會被包含在Go語言安裝包中。這裡介紹的所有包都可以使用,儘管在我寫這本書的時候有些包還不是很完整。

9.4.1 歸檔和壓縮包

Go語言提供了用於讀寫tar包文件和.zip文件的包archive/tar和archive/zip,如果需要壓縮tar包,還可以使用compress/gzip和compress/bzip2,這本書第8章的pack和unpack例子就涵蓋了這些功能的用法(參見8.2節)。

其他的壓縮格式也支持,例如LZW格式。compress/lzw包主要是用來處理.tiff圖像和.pdf文件。

9.4.2 字節流和字符串相關的包

bytes和strings這兩個包有很多函數是一樣的,只不過前者是處理byte類型的值,而後者是處理string 類型的值。對字符串來說,strings 包提供了大部分常用的功能,例如查找子串、替換子串、切割字符串、過濾字符串、改變大小寫(參見3.6.1節),等等。還有,利用strconv包可以很方便地將數值和布爾型類型的值轉換成字符串,反過來也可以(參見3.6.2節)。

fmt包提供了很多非常有用的打印函數和掃瞄函數。打印函數在第3章已經講過,掃瞄函數在表8-2也列出來了,後面還緊接著有一些用例。

unicode 包可以用來判斷字符的屬性,比如一個字符是否是可打印的,或者是否是一個數字(參見3.6.4節)。unicode/utf8和unicode/utf16這兩個包主要用來編碼和解碼rune (也就是Unicode碼點),其中unicode/utf8參見3.6.3節,部分還在第8章的utf16-to-utf8練習中出現過。

text/template和html/template包可以用來創建模板,借助模板可以很容易地輸出比如HTML等這些文檔,只要將一些數據填充進去即可。下面是一個非常簡單的text/template包的例子。

type GiniIndex struct {

Country string

Index float64

}

gini := GiniIndex{{〞Japan〞, 54.7}, {〞China〞, 55.0}, {〞U.S.A.〞, 80.1}}

giniTable := template.New(〞giniTable〞)

giniTable.Parse(

'<TABLE>' +

'{{range.}}' +

'{{printf 〞<TR><TD>%s</TD><TD>%.1f%%</TD></TR>〞.Country.Index}}'+

'{{end}}' +

'</TABLE>')

err := giniTable.Execute(os.Stdout, gini)

<TABLE>

<TR><TD>Japan</TD><TD>54.7%</TD></TR>

<TR><TD>China</TD><TD>55.0%</TD></TR>

<TR><TD>U.S.A.</TD><TD>80.1%</TD></TR>

</TABLE>

template.New函數根據給定的模板名創建了一個新的*template.Template 類型的值。模板名用於在模板嵌套的時候標識特定模板。template.Template.Parse函數解析一個模板(通常是一個.html文件)以備使用。template.Template.Execute函數執行一個模板,並從它的第二個參數讀取數據,最後將結果發送到給定的io.Writer 裡去。在這個例子裡,從 gini 切片讀取數據,gini 是一個 GiniIndex 結構體,然後將結果輸出到os.Stdout。(為了清晰起見,我們將輸出結果分成一行一行地顯示。)

模板裡的所有動作都是在一個雙大括號{{...}}裡的,還可以使用{{range}}...{{end}}來遍歷一個切片裡的所有項,這裡我們使用點號(.)來表示 GiniIndex 切片裡的每一項,也就是,這個點號可以理解為當前的項。我們可以使用字段名來訪問一個結構體的可導出的字段,當然,點號表示當前的項。{{printf}}動作和fmt.Printf函數是一樣的,只是使用空格符號來表示圓括號和參數分隔符。

text/template和html/template包支持原始的模板語言和很多動作,包括遍歷和條件分支,支持變量和方法調用,還有許多。此外,html/template包還可以安全地防止代碼注入。

9.4.3 容器包

Go語言裡的切片是一種非常高效的集合類型,但有些時候自定義一些特別的集合類型是有用的,或者是必需的。大部分情況下用內置的map就能解決很多問題,不過Go語言還是提供了container包來支持更多的容器類型。

我們可以使用container/heap包提供的函數來操作一個堆,前提是這個堆上的元素的類型必須滿足 heap包中 heap.Interface接口的定義。堆(嚴格來說是最小堆)維護了一個有序數組,保證堆上的第一個元素肯定是最小的(如果是最大堆,則第一個元素是最大的),這是大家熟知的堆的特性。heap.Interface 接口嵌入了 sort.Interface接口,並增加了 Push和Pop方法(其中 sort.Interface 我們在 4.2.4 節和5.7節講解過)。

要創建一個滿足heap.Interface接口定義的堆還蠻簡單的,這是一個例子。

ints := &IntHeap{5, 1, 6, 7, 9, 8, 2, 4}

heap.Init(ints) // 將其轉換成堆

ints.Push(9)  // IntHeap.Push並未保持堆的屬性

ints.Push(7)

ints.Push(3)

heap.Init(ints) // 堆被打破後必須重新將其轉換成堆

for ints.Len > 0 {

fmt.Printf(〞%v 〞, heap.Pop(ints))

}

fmt.Println // 打印1 2 3 4 5 6 7 7 8 9 9

下面是一個完整的自定義堆實現。

type IntHeap int

func (ints *IntHeap) Less(i, j int) bool {

return (*ints)[i] < (*ints)[j]

}

func (ints *IntHeap) Swap(i, j int) {

(*ints)[i], (*ints)[j] = (*ints)[j], (*ints)[i]

}

func (ints *IntHeap) Len int {

return len(*ints)

}

func (ints *IntHeap) Pop interface{} {

x := (*ints)[ints.Len-1]

*ints = (*ints)[:ints.Len-1]

return x

}

func (ints *IntHeap) Push(x interface{}) {

*ints = append(*ints, x.(int))

}

大多時候實現一個這樣的堆能夠解決很多問題了。為了讓代碼的可讀性更高一些,我們將這個堆定義為IntHeap struct { ints int },這樣我們就可以在方法裡引用ints.ints而不是*ints。

container/list 包提供了雙向鏈表的支持,可以將一個值以 interface{}的類型添加到鏈表裡去。從list裡得到的項的類型是list.Element,可以使用list.Element.Value來訪問我們添加進去的值。

items := list.New

for _, x := range strings.Split(〞ABCDEFGH〞, 〞〞) {

items.PushFront(x)

}

items.PushBack(9)

for element := items.Front; element != nil; element = element.Next {

switch value := element.Value.(type) {

case string:

fmt.Printf(〞%s 〞, value)

case int:

fmt.Printf(〞%d 〞, value)

}

}

fmt.Println // 打印H G F E D B A 9

在這個例子裡我們將8個字母依次添加到鏈表裡的最前端,並同時添加一個int型值在最後端。然後我們遍歷列表裡的元素將每個元素的值打印出來,這裡我們不需要使用類型開關,因為我們可以使用fmt.Printf(〞%v 〞, element.Value)。但如果我們不僅僅是只打印出它的值,還有其他的用途,這時候就得做類型開關。當然,如果所有的類型都是一樣的話,我們可以使用一個類型斷言,例如 element.Value.(string)可以用來判斷字符串。(關於類型開關我們在5.2.2.2節講過,類型斷言則在5.1.2節。)

除了上面我們剛介紹過的,list.List類型還提供了很多方法,包括Back、Init (用來清空一個列表)InsertAfter、InsertBefore、Len、MoveToBack、PushBackList(將一個列表添加到另一個列表的末尾)。

標準庫還提供了container/ring包,實現了一個環形的單向列表。[2]

因為這些容器類型的所有數據都是保存在內存的,如果數據量很大,可以考慮使用標準庫裡提供的database/sql包來將數據存儲到數據庫裡,database/sql實現了一個SQL數據庫的通用接口。實際使用的時候還必須安裝數據庫的相關驅動才行,這些和很多其他的容器包一樣,可以從Go Dashboard(godashboard.appspot.com/project)裡下載。還有就是之前我們看到的,本書包含了一個有序映射omap.Map類型,它基於左傾紅黑樹(left-leaning red-black Tree,參見6.5.3節)。

9.4.4 文件和操作系統相關的包

標準庫裡提供了很多包來支持文件和目錄相關的處理,以及一些和操作系統交互的系統調用。大多數情況下這些包都提供了一個平台無關的抽像層,方便我們寫出跨平台的代碼。

os(operating system)包提供了很多操作系統相關的函數,例如更改當前工作目錄,修改文件權限和所有者,讀取和設置環境變量,還有創建、刪除文件和目錄。另外,os包還提供了創建和打開文件(os.Create和os.Open)、獲取文件屬性(例如通過os.FileInfo類型)相關的函數,這些在我們之前的章節裡都全部用過了(參見7.2.5、第8章)。

一旦文件被打開了,特別是文本文件,經常需要用到一個緩衝區來訪問它的內容(例如讀取一行字符串而不是一個字節切片)。我們可以通過使用bufio包來實現這個功能,之前就有好些例子是這麼用的了。除了使用bufio.Reader和bufio.Writer 來讀寫字符串,我們還可以讀取(或者倒退)rune、單個字節、多個字節,還可以寫一個rune、一個或者多個字節。

io包提供了大量的與輸入輸出相關的函數,用來處理io.Reader和io.Writer。(*os.File類型的值能同時滿足這兩個接口的定義。)比如我們可以使用io.Copy函數來將數據從一個reader複製到一個writer裡去(參見8.2.1節)。另外,這個包還能用來創建內存中的同步管道。

io/ioutil 包提供了一些高級的輔助函數,例如,可以使用 ioutil.ReadAll函數來將一個io.Reader的所有數據讀取到一個byte中。ioutil.ReadFile函數也是同樣的功能,只是參數必須是字符串,也就是文件名,而不是一個 io.Reader。ioutil.TempFile函數用來創建一個臨時文件,返回*os.File 類型的值,還有ioutil.WriteFile函數可以將一個byte寫到指定的文件裡去。

path包用來操作Unix風格的路徑,例如Linux和Mac OS X路徑、URL路徑、git引用、FTP文件,等等。還有path/filepath包提供了和path相同的函數(當然還有其他的),其目的是提供平台無關的路徑處理。這個包還提供了filepath.Walk函數用來遍歷讀取一個給定路徑下的所有文件和目錄信息,如我們之前在7.2.5節看過的。

runtime包含一些函數和類型,可以用來訪問Go語言的運行時系統。大部分都是一些高級功能,很多時候我們用不到。不過有兩個常量還是經常有用的,就是 runtime.GOOS和runtime.GOARCH,兩個都是字符串,前者的值可能是「Darwin」、「freebsd」、「linux」或者「windows」,後者可以是「386」、「amd64」、「arm」等。runtime.GOROOT函數返回GOROOT環境變量的值(如果為空則返回Go安裝環境的根目錄),還有runtime.Version函數返回當前Go語言的版本(字符串),之前我們在第7章還見過runtime.GOMAXPROCS和runtime.NumCPU函數來讓Go語言使用機器上所有的處理器。

文件格式相關的包

Go語言標準庫對文本文件(7位的ASCII編碼、UTF-8或者UTF-16)的支持是非常優秀的,對二進制文件的支持也一樣。Go語言提供了一些單獨的包來處理JSON和XML文件,還包括它自己的高效簡便的Go語言二進制格式(gob)。(這些格式,包括自定義的二進制格式,我們在第8章都講過了。)

另外,Go語言還提供了csv包用來讀取.csv文件(csv是「comma-separated values」的縮寫,即用逗號分隔的值)。這個包將文件當成是數據記錄處理,也就是每一行被認為是一條記錄,包含多個逗號分隔的字段值。這個包具有很大的通用性,例如,我們可以改變它的分隔符(用縮進或者其他的字符來取代逗號),還可以修改它對記錄和字段的讀寫方式。

encoding包裡有好幾個子包。其中一個就是encoding/binary,我們用來讀寫二進制數據(參見 8.1.5 節)。還有其他的一些子包用來編碼和解碼一些比較常見的格式,例如, encoding/base64包用來編碼和解碼URL,因為它經常會使用這種編碼。

9.4.5 圖像處理相關的包

Go語言的image包提供了一些高級的函數和數據類型,用來創建和保存圖像數據。還包括一些用來對常見圖像格式進行編解碼的包,例如image/jpeg和image/png。其中一些我們在之前的章節已經討論過了,比如9.1.2節和第7章的一個練習。

image/draw 包提供了一些基本的畫圖功能,例如我們之前在第 6 章見過的。第三方的freetype 包為畫圖增加了一些功能,它可以使用特定的TrueType 字體來畫文本,還有freetype/raster包可以畫行,畫立方體,甚至是二次曲線。(我們在9.2節已經講過如何獲取和安裝freetype包。)

9.4.6 數學處理包

math/big包可以創建沒有大小限制(僅受內存大小限制)的整數(bit.Int)和有理數(big.Rat)。這些已經在之前的2.3.1.1 節已經介紹過。math/big 還提供了big.ProbablyPrime函數。

math包提供了所有標準的數學處理函數,這些函數都是基於float64類型的,還有一些標準的常量,可以參見表2-8、表2-9和表2-10。

math/cmplx包提供了常見的複數相關函數(基於complex128類型),參見2.11節。

9.4.7 其他一些包

除了以上這些大致歸類在一起的包以外,標準庫也包含了一些相對有點獨立的包。

crypto包提供了MD5、SHA-1、SHA-224、SHA-256、SHA-384、SHA-512等哈希算法。每一個哈希算法都以一個獨立的包存在,例如 crypto/sha512。此外,我們還可以使用crypto包來進行加密和解密,例如使用AES算法、DES算法等,分別對應crypto/aes和crypto/des包。

exec包可用來運行外部的程序,當然這也可以使用os.StartProcess函數來完成,但是exec.Cmd類型相對來說更加易用一些。

flag包是一個命令行解析器,可以接收X11風格的選項(例如,-width,而不是GNU風格,比如-w和--width)。這個包只能打印一條非常基本的用法幫助信息,並且沒有提供任何輸入值的合法檢查相關的能力。(所以我們可以指定一個 int 選項,但無法指定什麼範圍的值是可接受的。)一些替代性的包在Go Dashboard(godashboard.appspot.com/project)上可以找到。

log 包可以用來做日誌記錄(默認輸出到 os.Stdout),可在程序退出或拋出異常的同時產生一條日誌。可以使用log.SetOutput函數將log包的輸出目標更改成任意的io.Writer。日誌的輸出格式為時間戳和消息體,不想顯示時間戳的話可以在第一條 log輸出前調用log.SetFlags(0)。我們還可以使用log.New來創建自定義的logger實例。

math/rand包可以生成偽隨機數。rand.Int返回隨機的int型值,rand.Intn(n)返回一個在區間[0,n)範圍內的int型值。crypto/rand包還提供了函數用來產生加解密用途的更高強度的偽隨機數。

regexp包實現了一個非常快而強大的正則表達式引擎,支持RE2引擎語法。我們這本書就有好幾個地方是用到這個包的,雖然為了不跑題我們只是簡單地用了一下正則的功能,並沒用到這個包全部的特性。這個包之前也介紹過了(參見3.6.5節)。

sort包可以很方便地對切片進行排序,包括int型的、float64型的和string類型的,並且提供了基於已排序的切片上的快速查找功能(基於二分查找)。還提供了通用的sort.Sort函數和sort.Search函數用來處理自定義的數據類型(參見4.2.4節的例子和表4-2以及5.6.7節)。

time 包主要包括計時和日期時間解析及格式化相關的函數。time.After函數可以在指定時間間隔(就是我們傳入的納秒值參數)之後往該函數返回的通道裡發送一個當時的時間值,我們在之前的例子裡有介紹過它(參見7.2.2節)。time.Tick和time.NewTicker函數同樣返回一個信道,不同的是我們可以定期地從這個信道裡得到一個「嘀嗒」。time.Time結構實現了一些方法,例如,可以提供當前時間,格式化日期/時間為一個字符串,解析日期/時間。(我們在第8章看過time.Time的用法。)

9.4.8 網絡包

Go標準庫裡還有很多包支持網絡相關的編程。比如net包提供了一些通信相關的函數和數據類型,包括Unix域、網絡套接字,以及TCP/IP和UDP通信等。還有一些函數用來進行域名解析。

net/http包使用了net包,提供瞭解析HTTP請求和響應的功能,並提供了一個基礎的HTTP客戶端。除此之外,還包含了一個易於擴展的HTTP服務器,就像我們在第2 章和第3章的練習見到的那樣。net/url包提供了URL解析和查詢字符串的轉義。

標準庫裡還有其他一些高級的網絡包,其中一個就是net/rpc(遠程過程調用),可以讓客戶端遠程調用服務器上某些對象的導出方法。另一個就是net/smtp包(簡單郵件傳輸協議),用來發送郵件。

9.4.9 反射包

反射包可以提供運行時的反射(reflection)功能(也叫類型檢視,introspection),我們可以在運行時訪問和操作任意類型的值。

這個包也提供了一些非常有用的功能。例如 reflect.DeepEqual函數可以用來比較兩個值,例如不能直接使用==或者!=操作符進行比較的兩個切片。

Go語言裡每一個值都有兩個屬性:實際的值和它的類型。reflect.TypeOf函數能告訴我們任意值的類型。

x := 8.6

y := float32(2.5)

fmt.Printf(〞var x %v = %v\n〞, reflect.TypeOf(x), x)

fmt.Printf(〞var y %v = %v\n〞, reflect.TypeOf(y), y)

var x float64 = 8.6

var y float32 = 2.5

這裡我們使用反射功能輸出兩個var聲明的浮點變量和它們的類型。

調用reflect.ValueOf函數可以得到一個reflect.Value結構,這個結構保存了傳入的值,但並不是傳入的那個值本身。如果我們需要訪問那個傳入的值,必須使用 reflect.Value的方法。

word := 〞Chameleon〞

value := reflect.ValueOf(word)

text := value.String

fmt.Println(text)

Chameleon

reflect.Value類型實現了很多方法可以用來提取底層類型的實際值,包括reflect.Value.Bool、reflect.Value.Complex、reflect.Value.Float、reflect.Value.Int和reflect.Value.String。

同樣,reflect包還可以用於集合類型,例如切片、映射以及結構體。它甚至能訪問結構體的標籤文本(tag text)。(如我們在第8章見到的,json和xml的編碼器和解碼器使用了這個功能。)

type Contact struct {

Name string 〞check:len(3,40)〞

Id int 〞check:range(1,999999)〞

}

person := Contact{〞Bjork〞, 0xDEEDED}

personType := reflect.TypeOf(person)

if nameField, ok := personType.FieldByName(〞Name〞); ok {

fmt.Printf(〞%q %q %q\n〞, nameField.Type, nameField.Name, nameField.Tag)

}

〞string〞 〞Name〞 〞check:len(3,40)〞

如果一個被reflect.Value保存的底層類型的值是可設置的,那麼我們可以改變它。這種可設置的能力可以使用reflect.Value.CanSet來檢查,它返回一個bool類型的值。

presidents := string{〞Obama〞, 〞Bushy〞, 〞Clinton〞}

sliceValue := reflect.ValueOf(presidents)

value = sliceValue.Index(1)

value.SetString(〞Bush〞)

fmt.Println(presidents)

[Obama Bush Clinton]

儘管在Go語言裡字符串是不可修改的,但在一個string裡任意一個項都可以被其他的字符串替換,這也是我們這裡所做的。(自然地,在這個例子裡我們可以更直接地使用presidents[1] = 〞Bush〞來修改它,沒必要使用反射。)

因為無法修改不可修改的值的內容,我們可以在得到原始值的地址後直接用另一個值來替換它。

count := 1

if value = reflect.ValueOf(count); value.CanSet {

value.SetInt(2) // 會拋出異常,不能設置一個int值

}

fmt.Print(count, 〞 〞)

value = reflect.ValueOf(&count)

// 不能調用SetInt,因為值是一個*int而不是int

pointee := value.Elem

pointee.SetInt(3) // 成功。可以替換一個指針指向的值

fmt.Println(count)

1 3

從這段代碼可以看出,如果條件判斷失敗,則條件語句的主體部分不會被執行。儘管我們不能設置不可修改的值,如int、float64和string,但我們可以使用reflect.Value.Elem方法來獲得reflect.Value的值,這樣實質上就允許我們修改一個指針指向的值了,也就是我們這塊代碼所做的。

同樣我們可以用反射來調用任意的函數和方法。下面就是一個例子,它調用了兩次一個自定義的TitleCase函數(代碼沒有展示出來),一次是直接調用,一次使用了反射。

caption := 〞greg egan's dark integers〞

title := TitleCase(caption)

fmt.Println(title)

titleFuncValue := reflect.ValueOf(TitleCase)

values := titleFuncValue.Call(reflect.Value{reflect.ValueOf(caption)})

title = values[0].String

fmt.Println(title)

Greg Egan's Dark Integers

Greg Egan's Dark Integers

reflect.Value.Call方法傳入和返回參數都是一個reflect.Value切片。在這個例子中我們傳入了單個值(也就是一個長度為1的切片),並且得到一個結果值。

類似地,我們還能調用方法。實際上,我們甚至可以查詢某個方法是否存在,進而再決定是否調用它。

a := list.New               // a.Len == 0

b := list.New

b.PushFront(1)                // b.Len == 1

c := stack.Stack{}

c.Push(0.5)

c.Push(1.5)                 // c.Len == 2

d := map[string]int{〞A〞: 1, 〞B〞: 2, 〞C〞: 3} // len(d) == 3

e := 〞Four〞                 // len(e) == 4

f := int{5, 0, 4, 1, 3}          // len(f) == 5

fmt.Println(Len(a), Len(b), Len(c), Len(d), Len(e), Len(f))

0 1 2 3 4 5

這裡創建了兩個列表(使用container/list包),其中一個列表我們添加了一個項進去。我們也創建一個棧(使用在1.5節創建的自定義的stacker/stack包),然後往裡面增加兩個項。接著我們還創建了一個映射、一個字符串和一個int切片。所有這些值的長度都不同。

func Len(x interface{}) int {

value := reflect.ValueOf(x)

switch reflect.TypeOf(x).Kind {

case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:

return value.Len

default:

if method := value.MethodByName(〞Len〞); method.IsValid {

values := method.Call(nil)

return int(values[0].Int)

}

}

panic(fmt.Sprintf(〞'%v' does not have a length〞, x))

}

這個函數返回傳入值的長度,如果傳入的這個值的類型不支持獲得它的長度,就拋出一個異常。

首先我們獲得一個reflect.Value值(後面會用到)。然後我們使用switch語句,根據這個值的reflect.Kind 來進行條件處理。如果這個值的類型是支持內置 len函數的Go語言內置類型,我們直接調用 reflect.Value.Len函數。否則,這個類型要麼不支持獲得長度,要麼沒有實現Len方法。我們使用reflect.Value.MethodByName方法來獲得某個指定的方法,可能得到一個不合法的reflect.Value值,如果這個方法是合法的,我們就調用它。這裡我們不需要傳入任何參數,因為Len方法本身是不帶參數的。

我們調用reflect.Value.MethodByName方法得到的reflect.Value值同時保存了方法和值,所以當我們調用 reflect.Value.Call時,這個值可以直接作為接收者(receiver)。

reflect.Value.Int方法返回一個int64類型的值,我們還必須將它轉換成int型以匹配Len函數的返回值類型。

如果我們傳入一個不支持內置 len函數的值,也沒有 Len方法,那麼就會拋出一個異常。當然我們也可以使用其他的方式來處理這些錯誤。例如,返回-1表明沒有可用的長度,或者返回一個int和一個error值。

毫無疑問,Go語言的反射包是極其靈活的,允許我們在運行狀態做很多事情。但是,引用Rob Pike的一句話[3]:「對於這種強大的工具,我們應當謹慎地使用它們,除非有絕對的必要。」

9.5 練習

本章有3個相互關聯的練習,第一個練習要求創建一個自定義的包,第二個練習要求為這個包創建一個測試用例,第三個練習就是利用這個包來寫一個程序。這3個練習的難度不斷增加,尤其最後一個,很具有挑戰性。

(1)創建一個包,比如叫my_linkutil(在文件my_linkutil/my_linkutil.go裡)。同時這個包必須提供兩個函數,第一個是 LinksFromURL(string) (string, error),給定一個URL字符串(如http://www.qtrac.eu/index.html),然後返回這個頁面上消重後的所有鏈接(也就是標籤的href屬性值)和nil(或者返回nil和一個error,如果有錯誤發生的話)。第二個函數是LinksFromReader(io.Reader) (string, error),也是做相同的事情,只是從 io.Reader 裡讀取數據,比如可能是文件,或者一個 http.Response.Body。LinksFromURL函數可以調用LinksFromReader來完成大部分功能。

參考答案在linkcheck/linkutil/linkutil.go文件裡,第一個函數大約11行代碼,使用了 net/http 包的http.Get函數,第二個函數大概是 16 行代碼,使用了regexp.Regexp.FindAllSubmatch函數。

(2)Go標準庫提供了HTTP測試的支持(例如net/http/httptest包),不過我們這個練習只需測試第一題開發的my_linkutil.LinksFromReader函數。為該目的,請創建一個測試文件,比如 my_linkutil/my_linkutil_test.go,包含一個測試用例 TestLinksFromReader (*testing.T)。該測試從本地文件系統上讀取一個HTML文件和一個包含該HTML文件所有唯一鏈接的鏈接文件,然後對比my_linkutil.LinksFromReader 函數分析HTML文件的結果和這個鏈接文件中的鏈接。

可以複製 linkcheck/linkutil/index.html 文件和linkcheck/linkutil/index.links文件到my_linkutil目錄,用來當測試程序的數據文件。

參考答案在 linkcheck/linkutil/linkutil_test.go 裡,答案裡的測試函數大約 40行代碼左右,使用sort.Strings函數對找到的結果進行排序,並使用reflect.DeepEqual函數將結果與預期的結果進行對比。如果測試失敗,會列出不匹配的鏈接,方便測試人員測試。

(3)編寫一個程序,比如叫my_linkcheck,從命令行讀取一個URL(可以有http:// 前綴也可以沒有),然後檢查每個鏈接是否是有效的。程序可以使用遞歸,檢查每個鏈接到的頁面,但是不檢查非HTTP鏈接、非HTTP文件及外部網站的鏈接。應該使用一個獨立的goroutine來檢查一個頁面,這樣能實現並發的網絡訪問,比順序的一個一個來要快得多了。自然地,可能有多個頁面都包含相同的鏈接,但我們只需要檢查一次。這個程序應該使用第一道練習開發的my_linkutil包。

參考答案在linkcheck/linkcheck.go裡,大約150行代碼。為了避免檢查重複的鏈接,參考答案裡使用了一個映射來維護所有檢查過了的URL 列表。這個映射在一個獨立的goroutine裡維護,並使用3個信道來和它通信:一個用於增加URL,一個用於查詢URL是否存在,一個用於返回查詢結果。(另外一種方法就是使用第7章的safemap。)下面是一個從命令行輸入linkcheck www.qtrac.eu的結果(其中有些行已經被全部或部分的刪除)。

+ read http://www.qtrac.eu

...

+ read http://www.qtrac.eu/gobook.html

+ read http://www.qtrac.eu/gobook-errata.html

...

+ read http://www.qtrac.eu/comparepdf.html

+ read http://www.qtrac.eu/index.html

...

+ links on http://www.qtrac.eu/index.html

+ checked http://ptgmedia.pearsoncmg.com/.../python/python2python3.pdf

+ checked http://www.froglogic.com

- can't check non-http link: mailto:[email protected]

+ checked http://savannah.nongnu.org/projects/lout/

+ read http://www.qtrac.eu/py3book-errata.html

+ links on http://www.qtrac.eu

+ checked http://endsoftpatents.org/innovating-without-patents

+ links on http://www.qtrac.eu/gobook.html

+ checked http://golang.org

+ checked http://www.qtrac.eu/gobook.html#eg

+ checked http://www.informit.com/store/product.aspx?isbn=0321680561

+ checked http://safari.informit.com/9780321680563

+ checked http://www.qtrac.eu/gobook.tar.gz

+ checked http://www.qtrac.eu/gobook.zip

- can't check non-http link: ftp://ftp.cs.usyd.edu.au/jeff/lout/

+ checked http://safari.informit.com/9780132764100

+ checked http://www.qtrac.eu/gobook.html#toc

+ checked http://www.informit.com/store/product.aspx?isbn=0321774637

...


[1].文檔截屏展示了本書寫作時的godoc的HTML渲染結果,現在可能已經發生了改變。

[2].舊版本的Go語言還包含有container/vector包。現在這個包被廢棄了,可以使用切片和內置的append函數。

[3].關於Go語言的反射,Rob Pike寫了一篇有趣而實用的博客,見blog.golang.org/2011/ 09/laws-of-reflection.html。