這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文翻譯自Dr.Dobb's的"A Brief Tour of the Go Standard Library"一文。
在Go語言五周系列教程的最後一部分中,我們將帶領大家一起來瀏覽一下Go語言豐富的標準庫。
Go標準庫包含了大量包,提供了豐富廣泛的功能特性。這裡提供了概覽僅僅是有選擇性的且非常簡單。本文發表後,標準庫的內容還可能繼續增加,因此 建議大家最好是通過線上查閱庫API或使用godoc(包含在Go發布包中)來擷取最新資訊以及全面瞭解每個包所具備的功能。
exp包(實驗性的)是那些未來可能被加入標準庫的包起步的地方,因此除非你想參加這些包的開發(通過測試、討論、提交補丁),否則不應該使用其 下面的包。exp包通常只存在於從Google Go源碼樹上籤出的源碼包中,但一般不會包含在預構建好的包中。其他包可以放心使用,雖然在寫下本文的這一刻,很多包依舊不夠完整。
Archive(歸檔)和Compression(壓縮)包
Go支援讀寫tarball和.zip檔案。與此相關的包為archive/tar和archive/zip;以及用於壓縮tarball的 compress/gzip和compress/bzip2。
Go同樣也支援其他壓縮格式;例如用於TIFF映像和PDF檔案的Lempel-Ziv-Welch (compress/lzw)格式。
Bytes(位元組)和String(字串)相關包
bytes和strings包中有很多相同的函數,但前者操作的是[]byte類型的值,而後者操作的是string類型的值。strings包 提供了所有最有用的功能函數,諸如尋找子字串、替換子字串、拆分字串、剔除字串以及大小寫變換等。strconv包提供了數字和布爾類型 與string類型相互轉換的功能。
fmt包提供了大量有用的print和scan函數,它們在本系列教程的第一和第二部分已有相關介紹。
unicode包提供一些用於確定字元屬性的函數,諸如判斷一個字元是否是可列印的,或是否是一個數字。unicode/utf8與 unicode/utf16這兩個包提供了rune(即,Unicode碼點/字元)的編碼和解碼功能。
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(
'
' + '{{range .}}' + '{{printf "
%s |
%.1f%% |
"' + '.Country .Index}}'+ '{{end}}' + '
')
err := giniTable.Execute(os.Stdout, gini)
輸出:
Japan |
54.7% |
China |
55.0% |
U.S.A. |
80.1% |
template.New()函數用給定的名字建立了一個新的template.Template。模板名字用於識別模板,尤其是嵌入在其他模板 中時。template.Template.Parse()函數用於解析一個模板(通常從一個.html檔案中),解析後模板即可用。 template.Template.Execute()函數執行這個模板,將結果輸出到給定的io.Writer,並且從其第二個參數那裡讀取 用於產生模板的資料。在這個例子中,我將結果輸出到os.Stdout,並將GiniIndex類型的gini切片作為資料傳入(我將輸出拆分為 多行以便讓結果更加清晰)。
在模板內部,行為(action)包含在雙大括弧中({{和}})。{{range}} … {{end}}可用於迭代訪問一個切片中的每個元素。這裡我將切片中的每個GiniIndex設定為點(.);即是當前的元素。我們可以通過在名字訪問導 出欄位,當然名字前面需要用.來指明當前元素。{{printf}}的行為與fmt.Printf()函數類似,但用空格替換括弧以及用於分隔參 數的逗號。
text/template和html/template包自身支援一種複雜的範本語言,包括許多action,迭代和條件分支,支援變數和方法 調用,以及其它一些。除此之外,html/templage包還對代碼注入免疫。
集合包
切片是Go語言提供了最高效的集合類型,但有些時候使用一個更為特定的集合類型更有用或有必要。在多數情況下,內建的map類型已經足夠了,但Go標準庫還是提供了container包,其中包含了各種不同的集合包。
container/heap包提供了操作heap(堆)的函數,這裡heap必須是一個自訂類型的值,該類型必須滿足定義在heap包中 heap.Interface。一個heap(嚴格地說是一個min-heap)按特定順序維護其中的值 – 即第一個元素總是heap中最小的(對於max-heap,應該是最大的)- 這就是熟知的heap屬性。heap.Interface中嵌入了sort.Interface以及Push()和Pop方法。
我們可以很容易地建立一個滿足heap.Interface的自訂heap類型。下面是一個正在使用的heap的例子:
ints := &IntHeap{5, 1, 6, 7, 9, 8, 2, 4}
heap.Init(ints) // Heapify
ints.Push(9) // IntHeap.Push() doesn't preserve the heap property
ints.Push(7)
ints.Push(3)
heap.Init(ints) // Must reheapify after heap-breaking changes
for ints.Len() > 0 {
fmt.Printf("%v ", heap.Pop(ints))
}
fmt.Println() // prints: 1 2 3 4 5 6 7 7 8 9 9
下面是完整的自訂heap實現。
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類型換為type IntHeap struct { ints []int},這樣代碼更漂亮,我們可以在方法內部使用ints.ints,而不再是*ints了。
container/list包提供了雙向鏈表。元素以interface{}類型的值加入鏈表。從鏈表中擷取的元素的類型為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() // prints: H G F E D B A 9
在這裡例子中,我們將8個單字母字串推入一個新鏈表的前端,將一個整型數推入尾端。接下來,我們迭代訪問鏈表中的元素並列印元素的值。我們不是真的需要 type switch,因為我們可以使用fmt.Printf(%v ", element.Value)列印元素值,但如果我們要做的不僅僅是列印的話,如果列表中包含的元素類型不同,我們將需要type switch。當然,如果所有的元素都具有同一類型,我們可以使用type assertion,例如對string類型元素,我們使用element.Value.(string)。
除了上述提到的方法之外,list.List類型還提供了其他許多方法,包括Back(),Init()(用於清理鏈 表),InsertAfter(),InsertBefore(),Len(),MoveToBack(),MoveToFront(),PushBackList() (用於將一個鏈表推入另外一個鏈表的尾端),以及Remove()。
標準庫還提供了container/ring包,這個包實現了一個環形鏈表。
然而所有的集合類型都將資料存放區在記憶體中,Go還提供了database/sql包,該包提供了一個通用的SQL資料庫介面。當與真實資料庫互動時,特定的資料庫驅動包必須被單獨安裝。這些包,以及其他許多集合包都放在了Go Bashboard上。
檔案,作業系統以及相關包
標準庫提供了許多支援檔案和目錄操作以及與作業系統互動的包。在許多情況下,這些包提供了作業系統無關的抽象使得建立跨平台Go應用更為簡單。
os(作業系統)包提供了與作業系統互動相關的函數,諸如改變當前工作目錄,修改檔案模式和所有權,擷取和設定環境變數,建立和刪除檔案和目錄等。
此外,該包還提供了建立和開啟檔案(os.Create()和os.Open())、擷取檔案屬性(例如,通過os.FileInfo類型),以及在之前系列文章中我們所見過的函數。
一旦檔案被開啟,尤其是對於那些文字檔,通過一個buffer來訪問該檔案是非常常見的情況(將讀取的行存入字串而不是byte切片)。我們需要的這 個功能由bufio包提供。除了用bufio.Reader和bufio.Writer進行讀寫字串外,我們還可以讀(不讀)rune,讀(不讀)單字 節,讀多位元組以及寫rune和寫單位元組以及多位元組。
io(input/output)包提供了大量的函數用於與io.Reader和io.Writer一起工作(這兩個介面都可以被os.File類型值滿 足)。例如,我們曾用過io.Copy()函數將資料從一個reader拷貝到一個writer中。這個包還包含了用於建立同步的記憶體管道(pipe)的函數。
io/iotuil包提供了一些非常易用的函數。其中,這個包提供的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()函數用於遞迴地對給定路徑下的所有檔案和目錄進行迭代訪問。
runtime包包含了許多函數和類型用於訪問Go的運行時系統。這裡面的大多數都是進階功能,在日常建立標準Go程式時不應該使用到這些功能。但是,一 些包中的常量可能十分有用 – 例如,字串runtime.GOOS(其值例如,"darwin," "freebsd," "linux," 或 "windows")和字串runtime.GOARCH(其值例如386," "amd64,"或 "arm")。runtime.GOROOT()函數返回GOROOT環境變數的值(或者如果該環境變數沒有設定,返回Go構建根目 錄),runtime.Version()返回Go版本(以一個字串形式)。runtime.GOMAXPROCS()和 runtime.NumCPU()函數保證Go使用機器的所有處理器,在Go的文檔中有詳盡解釋。
檔案格式相關包
Go提供出色的檔案處理功能,既可用於文字檔(使用7-bit ASCII編碼或UTF-8和UTF-16 Unicode編碼),也可用於二進位檔案。Go提供了專門的包,用於處理JSON和XML檔案以及它自己專有的快速、簡潔以及方便的Go二進位格式。此 外,Go提供了csv包用於讀取CSV(逗號分隔的值)檔案。這個包將這些檔案視為記錄(每行算作一個記錄),麼個記錄由多個(逗號分隔的)欄位組成。這 個包用途非常廣泛,例如,可以用它修改分隔字元(從逗號改為tab或其他字元),以及其他諸如如何讀寫記錄和欄位的方面。
encoding包包含許多子包,其中的encoding/binary包我們曾用於讀寫位元據。其他包提供了針對各種格式的編解碼功能 – 例如,encoding/base64包可以用於編碼和解碼我們日常常用的URL。
映像相關包
Go的image包提供了一些高層次的函數和類型,用於建立和持有映像資料。它還提供了一些包,可用於不同種類標準影像檔格式的編解碼,例如image/jpeg和image/png。
image/draw包提供了一些基本的繪圖函數。第三方的freetype包加入了更多繪圖函數。freetype自身可以使用任意指定TrueType字型繪製文本,freetype/raster包可以繪製線條以及立方和二次曲線。
數學包
math/big包提供了無限大(實際受限於記憶體)整型數(big.Int)以及有理數(big.Rat)。math包提供了所有標準數學函數(基於float64)以及一些標準常量。math/cmplx包提供一些用於複數計算的標準函數(基於complex128)。
其他雜項包
除了這些可以被粗略分組的包外,標準庫還包含了許多相對獨立的包。
crypto包提供了使用MD5, SHA-1, SHA-224, SHA-256, SHA-384以及SHA-512演算法的Hash(每個演算法由一個包提供,例如crypto/sha512)。此外,crypto還提供了用於加密和解密 的子包,這些包使用了不同演算法,諸如AES、DES等等。每個包都對應相應的名字(例如,crypto/aes和crypto/des)。
exec包用於運行外部程式。我們也可以使用os.StartProcess來完成這件事,但exec.Cmd類型用起來更加簡單。
flag包提供了一個命令列解析器。它接受X11風格的命令列選項(例如,-width,非GNU風格的-w以及–width)。這個包產生一個非常基 本的usage訊息並且沒有提供除實值型別之外的任何校正(因此,這個包可以用於指定一個int型選項,而不是用於檢查接受哪些值)。還有一些候選包可以在 Go Bashboard中找到。
log包提供了一些函數,用於記錄日誌資訊(預設輸出到os.Stdout)、結束程式或拋出異常(panick)並攜帶一條日誌資訊。log包輸出目標 可以使用log.SetOutput()函數變更為任何io.Writer。日誌資訊以一個時間戳記加後續訊息的格式輸出;時間戳記的分割符可以在調用第一個 log函數之前通過log.SetFlags(0)設定。通過log.New()函數我們還可以建立自訂的logger。
math/rand包提供許多有用的偽隨機數產生函數,包括返回一個隨機整型數的rand.Int()以及rand.Intn(n),後者返回[0,n]範圍內的一個隨機整數。crypto/rand包中有一個函數,可用於產生加密的強偽隨機數字。regexp包提供快速且強大的正則式引擎,並支援RE2引擎的文法。
sort包提供了許多方便易用的函數,用於對ints、float64以及string類型的切片進行排序,並且提供基於有序切片的高效(二分尋找)的尋找。它還提供了用於自訂資料的通用sort.Sort()和sort.Search函數。
time包提供了用於測量時間、解析和格式化日期,日期/時間以及時間值的函數。time.After()函數可用於在特定納秒後,向通道 (channel)發送目前時間。time.Tick()和time.NewTicker()函數可用於提供一個通道,它會返回在特定時間間隔後將 'tick'發送到該通道上。time.Time結構具有一些方法,可提供目前時間,將data/time格式化為一個字串以及解析data /time。
網路包
Go標準庫中有許多包用於支援網路以及相關方面的編程。net包提供的函數和類型可用於使用Unix域以及網路socket通訊、TCP/IP和UDP編程。
這個包還提供了用於網域名稱解析的函數。net/http包充分利用了net包,並提供瞭解析HTTP請求和應答的功能,並提供了一個基本的HTTP用戶端。net/http包也包含一個易於擴充的HTTP server。net/url包提供了URL解析和查詢轉義。
標準庫中還包含其他一些其他高層次的網路包。一個是net/rpc(遠端程序呼叫)包,它允許一個服務端提供匯出可被用戶端調用的方法的對象。另外一個是net/smtp(簡易郵件傳輸通訊協定)包,可用於發送email。
Reflect包
reflect包提供了運行時反射(或稱為自省);即,在運行時訪問和與任意類型的值互動的能力。
這個包還提供了一些有用的工具函數,諸如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
這裡我們使用reflection輸出兩個浮點變數和它們的類型,類似Go的變數聲明。
當將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包也可以與集合類型一起使用,比如切片和map,也可以與struct一起使用;它甚至可以訪問結構體tag的文本(這種能力被用到了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()來獲知,該函數返回一個布爾值。
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) // OK. 我們可以通過值指標替換
fmt.Println(count)
輸出:
1 3
這小段代碼的輸出表明如果條件運算式求值結果為false,其分支語句將不會被執行。雖然我們無法重新設定那些不可改變的值,諸如ints、 float64或字串,但我們可以使用reflect.Value.Elem()方法來擷取一個reflectValue,通過它我們可以重新設定該地 址上的值,這就是我們在這段代碼結尾處所做的。
我們還可以使用反射來調用任意函數和方法。這裡是一個例子,例子用兩次調用了自訂函數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包),我們給其中一個加入一個元素。我們還建立了一個stack,並向其中加入兩個元素。我們 接下來建立了一個map,一個字串以及一個int類型切片,它們長度各不相同。我們使用Len()函數擷取了它們的長度。
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類型值,因為我們後續需要這個值。接下來我們根據reflect.Kind做switch判斷。如果value的 kind是某支援內建len()函數的內建類型的話,我們可以在該值上直接調用reflect.Value.Len()函數。否則,我們要麼得到一個不支 持長度概念的類型,要麼是一個擁有Len()方法的類型。我們使用reflect.Value.MethodByName()方法來擷取這個方法-或者獲 取一個無效的reflect.Value。如果這個方法有效,我們就調用它。
這個例子用沒有任何參數傳入,因為傳統Len()方法不接收任何參數。當我們使用reflect.Value.MethodByName()方法擷取一個 方法時,返回的reflect.Value既持有方法,又持有這個value。因此當我們調用reflect.Value.Call()時,這個 value將傳入並作為接收者。
reflect.Value.Int()方法返回一個int64類型值;我們這裡已將其轉換成一個普通的int以匹配通用的Len()函數的傳回值類型。
如果一個傳入的值不支援內建的len()函數並且沒有Len()方法,通用的Len()將引發異常。我們本可以採用其他方式處理這個錯誤情況 – 例如,返回-1一表明"不支援長度",或返回一個整型值和一個錯誤碼。
Go的reflect包十分靈活,允許我們在運行時根據程式的動態狀態做一些事情。但是,這裡引用Rob Pike的觀點,反射是“一個強大的工具,需謹慎並盡量避免使用,除非非常必要。(Rob Pick撰寫了一篇非常有趣和實用的有關Go反射的部落格文章)。
結論
這篇文章給Go語言五周系列教程做了一個收尾。此時此刻,你應該對這門語言,其工具以及它的標準庫有了一個很好的感性認識了。- 甚至是如何在Google App Engine上編寫Go程式以運行一個Web應用。我希望你能認可這一點:Go是一門非常有趣的語言,它提供了一種編寫可移植的、本地代碼的愉快的方式。
2012, bigwhite. 著作權.