diff --git a/CONTRIBUTORS.html b/CONTRIBUTORS.html index bb6ae46..e47f49a 100644 --- a/CONTRIBUTORS.html +++ b/CONTRIBUTORS.html @@ -5,7 +5,7 @@ -
chai2010 <chaishushan@gmail.com>
CrazySssst
TODO
+Slice(切片)代表變長的序列,序列中每個元素都有相同的類型。一個slice類型一般寫作[]T,其中T代表slice中元素的類型;語法和數組很像隻是沒有長度而已。
+數組和slice之間有着緊密的聯繫。一個slice是一個輕量級的數據結果,提供了訪問數組子序列(或者全部)元素的功能,因爲slice的底層確實引用一個數組對象。一個slice有三個部分構成:指針、長度和容量。指針指向第一個元素對應的底層數組元素的地址,slice的第一個元素併不一定就是數組的第一個元素。長度對應slice中元素的數目;長度不能超過容量,容量一般是從slice的開始位置到底層數據的結尾位置。內置的len和cap函數分别返迴slice的長度和容量。
+多個slice之間可以共享底層的數據,併且引用的數組部分區間可能重疊。圖4.1顯示了表示一年中每個月份名字的字符串數組,還有重疊引用了該數組的兩個slice。數組這樣定義
+months := [...]string{1: "January", /* ... */, 12: "December"}
+
+因此一月份是months[1],十二月份是months[12]。通常,數組的第一個元素從索引0開始,但是月份一般是從1開始的,因此我們聲明數組時直接第0個元素,第0個元素會被自動初始化爲空字符串。
+slice的操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用於創建一個新的slice,引用s的從第i個元素開始到第j-1個元素的子序列。新的slice將隻有j-i個元素。如果i位置的索引被省略的話將使用0代替,如果j位置的索引被省略的話將使用len(s)代替。因此,months[1:13]切片操作將引用全部有效的月份,和months[1:]操作等價;months[:]切片操作則是引用整個數組。讓我們分别定義表示第二季度和北方夏天的slice,它們有重疊部分:
+Q2 := months[4:7]
+summer := months[6:9]
+fmt.Println(Q2) // ["April" "May" "June"]
+fmt.Println(summer) // ["June" "July" "August"]
+
+兩個slice都包含了六月份,下面的代碼是一個包含相同月份的測試(性能較低):
+for _, s := range summer {
+ for _, q := range Q2 {
+ if s == q {
+ fmt.Printf("%s appears in both\n", s)
+ }
+ }
+}
+
+如果切片操作長處cap(s)的上限將導致一個panic異常,但是超出len(s)則是擴展了slice,因此新slice的長度會變大:
+fmt.Println(summer[:20]) // panic: out of range
+
+endlessSummer := summer[:5] // extend a slice (within capacity)
+fmt.Println(endlessSummer) // "[June July August September October]"
+
+另外,字符串的切片操作和[]byte字節類型切片的切片操作是類似的。它們都寫作x[m:n],併且都是返迴一個原始字節繫列的子序列,底層都是共享之前的底層數組,因此切片操作對應常量時間複雜度。x[m:n]切片操作對於字符串則生成一個新字符串,如果x是[]byte的話則生成一個新的[]byte。
+因爲slice值包含指向第一個元素的指針,因此向函數傳遞slice將運行在函數內部脩改底層數組的元素。換句話説,複雜一個slice隻是對底層的數組創建了一個新的slice别名(§2.3.2)。下面的reverse函數在原內存空間將[]int類型的slice反轉,而且它可以用於任意長度的slice。
+gopl.io/ch4/rev
+
+// reverse reverses a slice of ints in place.
+func reverse(s []int) {
+ for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
+ s[i], s[j] = s[j], s[i]
+ }
+}
+
+這里我們反轉數組的應用:
+a := [...]int{0, 1, 2, 3, 4, 5}
+reverse(a[:])
+fmt.Println(a) // "[5 4 3 2 1 0]"
+
+一種將slice元素循環向左镟轉n個元素的方法是三次調用reverse反轉函數,第一次是反轉開頭的n個元素,然後是反轉剩下的元素,最後是反轉整個slice的元素。(如果是向右循環镟轉,則將第三個函數調用移到第一個調用位置就可以了。)
+s := []int{0, 1, 2, 3, 4, 5}
+// Rotate s left by two positions.
+reverse(s[:2])
+reverse(s[2:])
+reverse(s)
+fmt.Println(s) // "[2 3 4 5 0 1]"
+
+要註意的是slice類型的變量s和數組類型的變量a的初始化語法的差異。slice和數組的字面值語法很類似,它們都是用花括弧包含一繫列的初始化元素,但是對於slice併沒有指明序列的長度。這會隱式地創建一個合適大小的數組,然後slice的指針指向底層的數組。就像數組字面值一樣,slice的字面值也可以按順序指定初始化值序列,或者是通過索引和元素值指定,或者的兩種風格的混合語法初始化。
+和數組不同的是,slice不能比較,因此我們不能使用==操作符來判斷兩個slice是否有相同的元素。不過標準庫提供了高度優化的bytes.Equal函數來判斷兩個字節型slice是否相等([]byte),但是對於其他類型的slice,我們必鬚自己展開每個元素進行比較:
+func equal(x, y []string) bool {
+ if len(x) != len(y) {
+ return false
+ }
+ for i := range x {
+ if x[i] != y[i] {
+ return false
+ }
+ }
+ return true
+}
+
+上面關於兩個slice的深度相等測試,運行的時間併不比支持==操作的數組或字符串更多,但是爲何slice卻不支持比較運算符呢?這方面有兩個原因。第一個原因,一個slice的元素是間接引用的,一個slice甚至可以包含自身。雖然有很多辦法處理這種情形,但是沒有一個是簡單有效的。
+第二個原因,因爲slice的元素是間接引用的,一個固定值的slice在不同的時間可能包含不同的元素,因爲底層數組的元素可能會被脩改。併且Go語言中map等哈希表之類的數據結構的key隻做簡單的淺拷貝,它要求在整個聲明週期中相等的key必鬚對相同的元素。對於像指針或chan之類的引用類型,==相等測試可以判斷兩個是否是引用相同的對象。一個針對slice的淺相等測試的==操作符可能是有一定用處的,也能臨時解決map類型的key問題,但是slice和數組不同的相等測試行爲會讓人睏惑。因此,安全的做飯是直接禁止slice之間的比較操作。
+slice唯一合法的比較是和nil比較,例如:
+if summer == nil { /* ... */ }
+
+一個零值的slice等於nil。一個nil值的slice併沒有底層數組。一個nil值的slice的長度和容量都是0,但是也有非nil值的slice的長度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。與任意類型的nil值一樣,我們可以用[]int(nil)類型轉換表達式來生成一個對應類型slice的nil值。
+var s []int // len(s) == 0, s == nil
+s = nil // len(s) == 0, s == nil
+s = []int(nil) // len(s) == 0, s == nil
+s = []int{} // len(s) == 0, s != nil
+
+如果你需要測試一個slice是否是空的,使用len(s) == 0來判斷,而不是用s == nil來判斷。除了和nil相等比較外,一個nil值的slice的行爲和其它任意0産長度的slice一樣;例如reverse(nil)也是安全的。除了文檔已經明確説明的地方,所有的Go語言函數應該以相同的方式對待nil值的slice和0長度的slice。
+內置的make函數創建一個指定元素類型、長度和容量的slice。容量部分可以省略,在這種情況下,容量將等於長度。
+make([]T, len)
+make([]T, len, cap) // same as make([]T, cap)[:len]
+
+在底層,make創建了一個匿名的數組變量,然後返迴一個slice;隻有通過返迴的slice才能引用底層匿名的數組變量。在第一種語句中,slice是整個數組的view。在第二個語句中,slice隻引用了底層數組的前len個元素,但是容量將包含整個的數組。額外的元素是留給未來的增長用的。
TODO
+內置的append函數用於向slice追加元素:
+var runes []rune
+for _, r := range "Hello, 世界" {
+ runes = append(runes, r)
+}
+fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
+
+在循環中使用append函數構建一個有九個rune字符構成的slice,當然對應這個特殊的問題我們可以通過Go語言內置的[]rune("Hello, 世界")轉換操作完成。
+append函數對於理解slice底層是如何工作的非常重要,所以讓我們仔細査看究竟是發生了什麽。下面是第一個版本的appendInt函數,專門用於處理[]int類型的slice:
+gopl.io/ch4/append
+
+func appendInt(x []int, y int) []int {
+ var z []int
+ zlen := len(x) + 1
+ if zlen <= cap(x) {
+ // There is room to grow. Extend the slice.
+ z = x[:zlen]
+ } else {
+ // There is insufficient space. Allocate a new array.
+ // Grow by doubling, for amortized linear complexity.
+ zcap := zlen
+ if zcap < 2*len(x) {
+ zcap = 2 * len(x)
+ }
+ z = make([]int, zlen, zcap)
+ copy(z, x) // a built-in function; see text
+ }
+ z[len(x)] = y
+ return z
+}
+
+每次調用appendInt函數,必鬚先檢測slice底層數組是否有足夠的容量來保存新添加的元素。如果有足夠空間的話,直接擴展slice(依然在原有的底層數組之上),將新添加的y元素複製到新擴展的空間,併返迴slice。因此,輸入的x和輸出的z共享相同的底層數組。
+如果沒有足夠的增長空間的話,appendInt函數則會先分配一個足夠大的slice用於保存新的結果,先將輸入的x複製到新的空間,然後添加y元素。結果z和輸入的x引用的將是不同的底層數組。
+雖然通過循環複製元素更直接,不過內置的copy函數可以方便地將一個slice複製另一個相同類型的slice。copy函數的第一個參數是要複製的目標slice,第二個參數是源slice,目標和源的位置順序和dst = src賦值語句是一致的。兩個slice可以共享同一個底層數組,甚至有重疊也沒有問題。copy函數將返迴成功複製的元素的個數(我們這里沒有用到),等於兩個slice中較小的長度,所以我們不用擔心覆蓋會超出目的slice的范圍。
+爲了效率,新分配的數組一般略大於保存x和y所需要的最低大小。通過在每次擴展數組時直接將長度翻倍從而避免了多次內存分配,也確保了添加單個元素操的平均時間是一個常數時間。這個程序演示了效果:
+func main() {
+ var x, y []int
+ for i := 0; i < 10; i++ {
+ y = appendInt(x, i)
+ fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
+ x = y
+ }
+}
+
+每一次容量的變化都會導致重新分配內存和copy操作:
+0 cap=1 [0]
+1 cap=2 [0 1]
+2 cap=4 [0 1 2]
+3 cap=4 [0 1 2 3]
+4 cap=8 [0 1 2 3 4]
+5 cap=8 [0 1 2 3 4 5]
+6 cap=8 [0 1 2 3 4 5 6]
+7 cap=8 [0 1 2 3 4 5 6 7]
+8 cap=16 [0 1 2 3 4 5 6 7 8]
+9 cap=16 [0 1 2 3 4 5 6 7 8 9]
+
讓我們仔細査看i=3次的迭代。當時x包含了[0 1 2]三個元素,但是容量是4,因此可以簡單將新的元素添加到末尾,不需要新的內存分配。然後新的y的長度和容量都是4,併且和x引用着相同的底層數組,如圖4.2所示。
+在下一次迭代時i=4,現在沒有新的空餘的空間了,因此appendInt函數分配一個容量爲8的底層數組,將x的4個元素[0 1 2 3]複製到新空間的開頭,然後添加新的元素i,新元素的值是4。新的y的長度是5,容量是8;後面有3個空閒的位置,三次迭代都不需要分配新的空間。當前迭代中,y和x是對應不用底層數組的view。這次操作如圖4.3所示。
+內置的append函數可能使用比appendInt更複雜的內存擴展策略。因此,通常我們併不知道append調用是否導致了內存的分配,因此我們也不能確認新的slice和原始的slice是否引用的是相同的底層數組空間。同樣,我們不能確認在原先的slice上的操作是否會影響到新的slice。因此,通常是將append返迴的結果直接賦值給輸入的slice變量:
+runes = append(runes, r)
+
+更新slice變量不僅對調用append函數是必要的,實際上對應任何可能導致長度、容量或底層數組變化的操作都是必要的。要正確地使用slice,需要記住盡管底層數組的元素是間接訪問,但是slice本身的指針、長度和容量是直接訪問的。要更新這些信息需要像上面例子那樣一個顯式的賦值操作。從這個角度看,slice併不是一個純粹的引用類型,它實際上是一個類似下面結構體的聚合類型:
+type IntSlice struct {
+ ptr *int
+ len, cap int
+}
+
+我們的appendInt函數每次隻能向slice追加一個元素,但是內置的append函數則可以追加多個元素,甚至追加一個slice。
+var x []int
+x = append(x, 1)
+x = append(x, 2, 3)
+x = append(x, 4, 5, 6)
+x = append(x, x...) // append the slice x
+fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"
+
+通過下面的小脩改,我們可以可以達到append函數類似的功能。其中在appendInt函數參數中的最後的“...”省略號表示接收變長的參數爲slice。我們將在5.7節詳細解釋這個特性。
+func appendInt(x []int, y ...int) []int {
+ var z []int
+ zlen := len(x) + len(y)
+ // ...expand z to at least zlen...
+ copy(z[len(x):], y)
+ return z
+}
+
+爲了避免重複,和前面相同的代碼併沒有顯示。
TODO
+讓我們看看更多的例子,比如镟轉slice、反轉slice或在slice原有內存空間脩改元素。給定一個字符串列表,下面的nonempty函數將在原有slice內存空間之上返迴不包含空字符串的列表:
+gopl.io/ch4/nonempty
+
+// Nonempty is an example of an in-place slice algorithm.
+package main
+
+import "fmt"
+
+// nonempty returns a slice holding only the non-empty strings.
+// The underlying array is modified during the call.
+func nonempty(strings []string) []string {
+ i := 0
+ for _, s := range strings {
+ if s != "" {
+ strings[i] = s
+ i++
+ }
+ }
+ return strings[:i]
+}
+
+比較微妙的地方是,輸入的slice和輸出的slice共享一個底層數組。這可以避免分配另一個數組,不過原來的數據將可能會被覆蓋,正如下面兩個打印語句看到的那樣:
+data := []string{"one", "", "three"}
+fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
+fmt.Printf("%q\n", data) // `["one" "three" "three"]`
+
+因此我們通常會這樣使用nonempty函數:data = nonempty(data)。
+nonempty函數也可以使用append函數實現:
+func nonempty2(strings []string) []string {
+ out := strings[:0] // zero-length slice of original
+ for _, s := range strings {
+ if s != "" {
+ out = append(out, s)
+ }
+ }
+ return out
+}
+
+無論如何實現,以這種方式重用一個slice一般要求最多爲每個輸入值産生一個輸出值,事實上很多算法都是用來過濾或合併序列中相鄰的元素。這種slice用法是比較複雜的技巧,雖然使用到了slice的一些黑魔法,但是對於某些場合是比較清晰和有效的。
+一個slice可以原來實現一個stack。最初給定的空slice對應一個空的stack,然後可以使用append函數將新的值壓入stack:
+stack = append(stack, v) // push v
+
+stack的頂部位置對應slice的最後一個元素:
+top := stack[len(stack)-1] // top of stack
+
+通過收縮stack可以彈出棧頂的元素
+stack = stack[:len(stack)-1] // pop
+
+要刪除slice中間的某個元素併保存原有的元素順序,可以通過內置的copy函數將後面的子slice向前一位複雜完成:
+func remove(slice []int, i int) []int {
+ copy(slice[i:], slice[i+1:])
+ return slice[:len(slice)-1]
+}
+
+func main() {
+ s := []int{5, 6, 7, 8, 9}
+ fmt.Println(remove(s, 2)) // "[5 6 8 9]"
+}
+
+如果刪除元素後不用保存原來順序的話,我們可以簡單的用最後一個元素覆蓋被刪除的元素:
+func remove(slice []int, i int) []int {
+ slice[i] = slice[len(slice)-1]
+ return slice[:len(slice)-1]
+}
+
+func main() {
+ s := []int{5, 6, 7, 8, 9}
+ fmt.Println(remove(s, 2)) // "[5 6 9 8]
+}
+
+練習 4.3: 重寫reverse函數,使用數組指針代替slice。
+練習 4.4: 編寫一個rotate函數,通過一次循環完成镟轉。
+練習 4.5: 寫一個函數在原地完成消除[]string中相鄰重複的字符串的操作。
+練習 4.6: 編寫一個函數,原地將一個UTF-8編碼的[]byte類型的slice中相鄰的空格(參考unicode.IsSpace)替換成一個空格返迴
+練習 4.7: 脩改reverse函數用於原地反轉UTF-8編碼的[]byte。是否可以不用分配額外的內存?
TODO
+哈希表是一種巧妙併且實用的數據結構。它是一個無序的key/value對的集合,其中所有的key都是不同的,然後通過給定的key可以在常數時間複雜度內檢索、更新或刪除對應的value。
+在Go語言中,一個map就是一個哈希表的引用,map類型可以寫爲map[K]V,其中K和V分别對應key和value。map中所有的key都有相同的類型,所以的value也有着相同的類型,但是key和value之間可以是不同的數據類型。其中K對應的key必鬚是支持==比較運算符的數據類型,所以map可以通過測試key是否相等來判斷是否已經存在。雖然浮點數類型也是支持相等運算符比較的,但是將浮點數用做key類型則是一個壞的想法,正如第三章提到的,最壞的情況是可能出現的NaN和任何浮點數都不相等。對於V對應的value數據類型則沒有任何的限製。
+內置的make函數可以創建一個map:
+ages := make(map[string]int) // mapping from strings to ints
+
+我們也可以用map字面值的語法創建map,同時還可以指定一些最初的key/value:
+ages := map[string]int{
+ "alice": 31,
+ "charlie": 34,
+}
+
+這相當於
+ages := make(map[string]int)
+ages["alice"] = 31
+ages["charlie"] = 34
+
+因此,另一種創建空的map的表達式是map[string]int{}。
+Map中的元素通過key對應的下標語法訪問:
+ages["alice"] = 32
+fmt.Println(ages["alice"]) // "32"
+
+使用內置的delete函數可以刪除元素:
+delete(ages, "alice") // remove element ages["alice"]
+
+所有這些操作是安全的,卽使這些元素不在map中也沒有關繫;如果一個査找失敗將返迴value類型對應的零值,例如,卽使map中不存在“bob”下面的代碼也可以正常工作,因爲ages["bob"]失敗時將返迴0。
+ages["bob"] = ages["bob"] + 1 // happy birthday!
+
+而且x += y和x++等簡短賦值語法也可以用在map上,所以上面的代碼可以改寫成
+ages["bob"] += 1
+
+更簡單的寫法
+ages["bob"]++
+
+但是map中的元素併不是一個變量,因此我們不能對map的元素進行取址操作:
+_ = &ages["bob"] // compile error: cannot take address of map element
+
+禁止對map元素取址的原因是map可能隨着元素數量的增長而重新分配更大的內存空間,從而可能導致之前的地址無效。
+要想遍歷map中全部的key/value對的話,可以使用range風格的for循環實現,和之前的slice遍歷語法類似。下面的迭代語句將在每次迭代時設置name和age變量,它們對應下一個鍵/值對:
+for name, age := range ages {
+ fmt.Printf("%s\t%d\n", name, age)
+}
+
+Map的迭代順序是不確定的,併且不同的哈希函數實現可能導致不同的遍歷順序。在實踐中,遍歷的順序是隨機的,每一次遍歷的順序都不相同。這是故意的,每次都使用隨機的遍歷順序可以強製要求程序不會依賴具體的哈希函數實現。如果要按順序遍歷key/value對,我們必鬚顯式地對key進行排序,可以使用sort包的Strings函數對字符串slice進行排序。下面是常見的處理方式:
+import "sort"
+
+var names []string
+for name := range ages {
+ names = append(names, name)
+}
+sort.Strings(names)
+for _, name := range names {
+ fmt.Printf("%s\t%d\n", name, ages[name])
+}
+
+因爲我們一開始就知道names的最終大小,因此給slice分配一個合適的大小將會更有效。下面的代碼創建了一個空的slice,但是slice的容量剛好可以放下map中全部的key:
+names := make([]string, 0, len(ages))
+
+在上面的第一個range循環中,我們隻關心map中的key,所以我們忽略了第二個循環變量。在第二個循環中,我們隻關繫names中的名字,所以我們使用“_”空白標識符來忽略第一個循環變量,也就是迭代slice時的索引。
+map類型的零值是nil,也就是沒有引用任何哈希表。
+var ages map[string]int
+fmt.Println(ages == nil) // "true"
+fmt.Println(len(ages) == 0) // "true"
+
+map上的大部分操作,包括査找、刪除、len和range循環都可以安全工作在nil值的map上,它們的行爲和一個空的map類似。但是向一個nil值的map存入元素將導致一個panic異常:
+ages["carol"] = 21 // panic: assignment to entry in nil map
+
+在向map存數據前必鬚先創建map。
+通過key作爲索引下標來訪問map將産生一個value。如果key在map中是存在的,那麽將得到與key對應的value;如果key不存在,那麽將得到value對應類型的零值,正如我們前面看到的ages["bob"]那樣。這個規則很實用,但是有時候可能需要知道對應的元素是否眞的是在map之中。例如,如果元素類型是一個數字,你可以需要區分一個已經存在的0,和不存在而返迴零值的0,可以像下面這樣測試:
+age, ok := ages["bob"]
+if !ok { /* "bob" is not a key in this map; age == 0. */ }
+
+你會經常看到將這兩個結合起來使用,像這樣:
+if age, ok := ages["bob"]; !ok { /* ... */ }
+
+在這種場景下,map的下標語法將産生兩個值;第二個是一個布爾值,用於報告元素是否眞的存在。布爾變量一般命名爲ok,特别適合馬上用於if條件判斷部分。
+和slice一樣,map之間也不能進行相等比較;唯一的例外是和nil進行比較。要判斷兩個map是否包含相同的key和value,我們必鬚通過一個循環實現:
+func equal(x, y map[string]int) bool {
+ if len(x) != len(y) {
+ return false
+ }
+ for k, xv := range x {
+ if yv, ok := y[k]; !ok || yv != xv {
+ return false
+ }
+ }
+ return true
+}
+
+要註意我們是如何用!ok來區分元素缺失和元素不同的。我們不能簡單地用xv != y[k]判斷,那樣會導致在判斷下面兩個map時産生錯誤的結果:
+// True if equal is written incorrectly.
+equal(map[string]int{"A": 0}, map[string]int{"B": 42})
+
+Go語言中併沒有提供一個set類型,但是map中的key也是不相同的,可以用map實現類似的功能。爲了説明這一點,下面的dedup程序讀取多行輸入,但是隻打印第一次出現的行。(它是1.3節中出現的dup程序的變體。)dedup程序通過map來表示所有的輸入行所對應的set集合,以確保已經在集合存在的行不會被重複打印。
+gopl.io/ch4/dedup
+
+func main() {
+ seen := make(map[string]bool) // a set of strings
+ input := bufio.NewScanner(os.Stdin)
+ for input.Scan() {
+ line := input.Text()
+ if !seen[line] {
+ seen[line] = true
+ fmt.Println(line)
+ }
+ }
+
+ if err := input.Err(); err != nil {
+ fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+Go程序員將這種忽略value的map當作一個字符串集合,併非所有map[string]bool類型value都是無關緊要的;有一些則可能會同時包含tue和false的值。
+有時候我們需要一個map或set的key是slice類型,但是map的key必鬚是可比較的,但是slice併不滿足這個條件。不過,我們可以通過兩個步驟繞過這個限製。第一步,定義一個輔助函數k,將slice轉爲map對應的string類型的key,確保隻有x和y相等時k(x) == k(y)才成立。然後創建一個key爲string類型的map,在每次對map操作時先用k輔助函數將slice轉化爲string類型。
+下面的例子演示了如何使用map來記録提交相同的字符串列表的次數。它使用了fmt.Sprintf函數將字符串列表轉換爲一個字符串以用於map的key,通過%q參數忠實地記録每個字符串元素的信息:
+var m = make(map[string]int)
+
+func k(list []string) string { return fmt.Sprintf("%q", list) }
+
+func Add(list []string) { m[k(list)]++ }
+func Count(list []string) int { return m[k(list)] }
+
+使用同樣的技術可以處理任何不可比較的key類型,而不僅僅是slice類型。它對於想使用自定義key比較函數的時候也很有用,例如在比較字符串的時候忽略大小寫。同時,輔助函數k(x)也不一定是字符串類型,它可以返迴任何可比較的類型,例如整數、數組或結構體等。
+這是map的另一個例子,下面的程序用於統計輸入中每個Unicode碼點出現的次數。雖然Unicode全部碼點的數量鉅大,但是出現在特點文檔中的字符併沒有多少,使用map可以用比較自然的方式來跟蹤那些見過字符次數。
+gopl.io/ch4/charcount
+
+// Charcount computes counts of Unicode characters.
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "unicode"
+ "unicode/utf8"
+)
+
+func main() {
+ counts := make(map[rune]int) // counts of Unicode characters
+ var utflen [utf8.UTFMax + 1]int // count of lengths of UTF-8 encodings
+ invalid := 0 // count of invalid UTF-8 characters
+
+ in := bufio.NewReader(os.Stdin)
+ for {
+ r, n, err := in.ReadRune() // returns rune, nbytes, error
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "charcount: %v\n", err)
+ os.Exit(1)
+ }
+ if r == unicode.ReplacementChar && n == 1 {
+ invalid++
+ continue
+ }
+ counts[r]++
+ utflen[n]++
+ }
+ fmt.Printf("rune\tcount\n")
+ for c, n := range counts {
+ fmt.Printf("%q\t%d\n", c, n)
+ }
+ fmt.Print("\nlen\tcount\n")
+ for i, n := range utflen {
+ if i > 0 {
+ fmt.Printf("%d\t%d\n", i, n)
+ }
+ }
+ if invalid > 0 {
+ fmt.Printf("\n%d invalid UTF-8 characters\n", invalid)
+ }
+}
+
+ReadRune方法執行UTF-8解碼併返迴三個值:解碼的rune字符的值,字符UTF-8編碼後的長度,和一個錯誤值。我們可預期的錯誤值隻有對應文件結尾的io.EOF。如果輸入的是無效的UTF-8編碼的字符,返迴的將是unicode.ReplacementChar無效字符,併且編碼長度是1。
+charcount程序同時打印不同UTF-8編碼長度的字符數目。對此,map併不是一個合適的數據結構;因爲UTF-8編碼的長度總是從1到utf8.UTFMax(最大是4個字節),使用數組將更有效。
+作爲一個實驗,我們用charcount程序對本身的字符進行了統計。雖然大部分是英語,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符:
+下面是不同UTF-8編碼長度的字符的數目:
+len count
+1 765391
+2 60
+3 70
+4 0
+
Map的value類型也可以是一個聚合類型,比如是一個map或slice。在下面的代碼中,圖graph的key類型是一個字符串,value類型map[string]bool代表一個字符串集合。從概念上將,graph將一個字符串類型的key映射到一組相關的字符串集合,它們指向新的graph的key。
+gopl.io/ch4/graph
+
+var graph = make(map[string]map[string]bool)
+
+func addEdge(from, to string) {
+ edges := graph[from]
+ if edges == nil {
+ edges = make(map[string]bool)
+ graph[from] = edges
+ }
+ edges[to] = true
+}
+
+func hasEdge(from, to string) bool {
+ return graph[from][to]
+}
+
+其中addEdge函數惰性初始化map是一個慣用方式,也就是説在每個值首次作爲key是才初始化。addEdge函數顯示了如何讓map的零值也能正常工作;卽使from到to的邊不存在,graph[from][to]依然可以返迴一個有意義的結果。
+練習 4.8: 脩改charcount程序,使用unicode.IsLetter等相關的函數,統計字母、數字等Unicode中不同的字符類别。
+練習 4.9: 編寫一個程序wordfreq程序,報告輸入文本中每個單詞出現的頻率。在第一次調用Scan前先調用input.Split(bufio.ScanWords)函數,這樣可以按單詞而不是按行輸入。
TODO
+結構體是一種聚合的數據類型,由零個或多個任意類型的值聚合成的實體。每個值稱爲結構體的成員。是用結構體的經典案例處理公司的員工信息,每個員工信息包含一個唯一的員工編號、員工的名字、家庭住址、出生日期、工作崗位、薪資、上級領導等等。所有的這些成員都需要綁定到一個實體,可以作爲一個整體單元被複製,作爲函數的參數或返迴值,或者是被存儲到數組中,等等。
+下面兩個語句分别聲明了一個叫Employee的結構體類型,併且聲明了一個Employee類型的變量dilbert:
+type Employee struct {
+ ID int
+ Name string
+ Address string
+ DoB time.Time
+ Position string
+ Salary int
+ ManagerID int
+}
+
+var dilbert Employee
+
+dilbert結構體變量的成員可以通過點操作符訪問,比如dilbert.Name和dilbert.DoB。因爲dilbert是一個變量,它所有的成員也同樣是變量,我們可以對每個成員賦值:
+dilbert.Salary -= 5000 // demoted, for writing too few lines of code
+
+或者是對成員取地址,然後通過指針訪問:
+position := &dilbert.Position
+*position = "Senior " + *position // promoted, for outsourcing to Elbonia
+
+點操作符也可以和指向結構體的指針一起工作:
+var employeeOfTheMonth *Employee = &dilbert
+employeeOfTheMonth.Position += " (proactive team player)"
+
+相當於下面語句
+(*employeeOfTheMonth).Position += " (proactive team player)"
+
+EmployeeByID函數將根據給定的員工ID返迴對應的員工信息結構體的指針。我們可以使用點操作符來訪問它里面的成員:
+func EmployeeByID(id int) *Employee { /* ... */ }
+
+fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss"
+
+id := dilbert.ID
+EmployeeByID(id).Salary = 0 // fired for... no real reason
+
+後面的語句通過EmployeeByID返迴的結構體指針更新了Employee結構體的成員。如果將EmployeeByID函數的返迴值從*Employee
指針類型改爲Employee值類型,那麽更新語句將不能編譯通過,因爲在賦值語句的左邊併不確定是一個變量。
通常一行對應一個結構體成員,成員的名字在前類型在後,不過如果相鄰的成員類型如果相同的話可以被合併到一行,就像下面的Name和Address成員那樣:
+type Employee struct {
+ ID int
+ Name, Address string
+ DoB time.Time
+ Position string
+ Salary int
+ ManagerID int
+}
+
+結構體成員的輸入順序也有重要的意義。我們也可以將Position成員合併(因爲也是字符串類型),或者是交換Name和Address出現的先後順序,那樣的話就是定義了不同的結構體類型。通常,我們隻是將相關的成員合併到一起。
+如果結構體成員名字是以大寫字母開頭的,那麽該成員就是導出的;這是Go語言導出規則決定的。一個結構體可能同時包含導出和未導出的成員。
+結構體類型往往是冗長的,因爲它的每個成員可能都會占一行。雖然我們每次都可以重寫整個結構體成員,但是重複會令人厭煩。因此,完整的結構體寫法通常隻在類型聲明語句的地方出現,就像Employee類型聲明語句那樣。
+一個命名爲S的結構體類型將不能再包含S類型的成員:一個聚合的值不能包含它自身。(該限製同樣適應於數組。)但是S類型的結構體可以包含*S
指針類型的成員,這可以讓我們創建遞歸的數據結構,比如鏈表和樹結構等。在下面的代碼中,我們使用一個二叉樹來實現一個插入排序:
gopl.io/ch4/treesort
+
+type tree struct {
+ value int
+ left, right *tree
+}
+
+// Sort sorts values in place.
+func Sort(values []int) {
+ var root *tree
+ for _, v := range values {
+ root = add(root, v)
+ }
+ appendValues(values[:0], root)
+}
+
+// appendValues appends the elements of t to values in order
+// and returns the resulting slice.
+func appendValues(values []int, t *tree) []int {
+ if t != nil {
+ values = appendValues(values, t.left)
+ values = append(values, t.value)
+ values = appendValues(values, t.right)
+ }
+ return values
+}
+
+func add(t *tree, value int) *tree {
+ if t == nil {
+ // Equivalent to return &tree{value: value}.
+ t = new(tree)
+ t.value = value
+ return t
+ }
+ if value < t.value {
+ t.left = add(t.left, value)
+ } else {
+ t.right = add(t.right, value)
+ }
+ return t
+}
+
+結構體類型的零值是每個成員都對是零值。通常會將零值作爲最合理的默認值。例如,在bytes.Buffer類型,結構體初始值就是一個隨時可用的空緩存,還有在第9章將會講到的sync.Mutex的零值也是有效的未鎖狀態。有時候這種零值可用的特性是自然獲得的,但是也有些類型需要一些額外的工作。
+如果結構體沒有任何成員的話就是空結構體,寫作struct{}。它的大小爲0,也不包含任何信息,但是有時候依然是有價值的。有些Go語言程序員用map帶模擬set數據結構時,用它來代替map中布爾類型的value,隻是強調key的重要性,但是因爲節約的空間有限,而且語法比較複雜,所有我們通常避免避免這樣的用法。
+seen := make(map[string]struct{}) // set of strings
+// ...
+if _, ok := seen[s]; !ok {
+ seen[s] = struct{}{}
+ // ...first time seeing s...
+}
+
TODO
+結構體值可以用結構體面值表示,結構體面值可以指定每個成員的值。
+type Point struct{ X, Y int }
+
+p := Point{1, 2}
+
+這里有兩種形式的結構體面值語法,上面的是第一種寫法,要求以結構體成員定義的順序爲每個結構體成員指定一個面值。它要求寫代碼和讀代碼的人要記住結構體的每個成員的類型和順序,併且結構體成員有細微的調整就可能導致上述代碼不能編譯。因此,上述的語法一般隻在定義結構體的包內部使用,或者是在較小的結構體中使用,這些結構體的成員排列比較規則,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。
+其實更常用的是第二種寫法,以成員名字和相應的值來初始化,可以包含部分或全部的成員,如1.4節的Lissajous程序的寫法:
+anim := gif.GIF{LoopCount: nframes}
+
+在這種形式的結構體面值寫法中,如果成員被忽略的話將默認用零值。因爲,提供了成員的名字,所有成員出現的順序併不重要。
+兩種不同形式的寫法不能混合使用。而且,你不能企圖在外部包中用第一種順序賦值的技巧來偷偷地初始化結構體中未導出的成員。
+package p
+type T struct{ a, b int } // a and b are not exported
+
+package q
+import "p"
+var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
+var _ = p.T{1, 2} // compile error: can't reference a, b
+
+雖然上面最後一行代碼的編譯錯誤信息中併沒有顯式提到未導出的成員,但是這樣企圖隱式使用未導出成員的行爲也是不允許的。
+結構體可以作爲函數的參數和返迴值。例如,這個Scale函數將Point類型的值縮放後返迴:
+func Scale(p Point, factor int) Point {
+ return Point{p.X * factor, p.Y * factor}
+}
+
+fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
+
+如果考慮效率的話,較大的結構體通常會用指針的方式傳入和返迴,
+func Bonus(e *Employee, percent int) int {
+ return e.Salary * percent / 100
+}
+
+如果要在函數內部脩改結構體成員的話,用指針傳入是必鬚的;因爲在Go語言中,所有的函數參數都是值拷貝出入的,函數參數將不再是函數調用時的原始變量。
+func AwardAnnualRaise(e *Employee) {
+ e.Salary = e.Salary * 105 / 100
+}
+
+因爲結構體通常通過指針處理,可以用下面的寫法來創建併初始化一個結構體變量,併返迴結構體的地址:
+pp := &Point{1, 2}
+
+它是下面的語句是等價的
+pp := new(Point)
+*pp = Point{1, 2}
+
+不過&Point{1, 2}寫法可以直接在表達式中使用,比如一個函數調用。
TODO
+如果結構體的全部成員都是可以比較的,那麽結構體也是可以比較的,那樣的話兩個結構體將可以使用==或!=運算符進行比較。相等比較運算符==將比較兩個結構體的每個成員,因此下面兩個比較的表達式是等價的:
+type Point struct{ X, Y int }
+
+p := Point{1, 2}
+q := Point{2, 1}
+fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
+fmt.Println(p == q) // "false"
+
+可比較的結構體類型和其他可比較的類型一樣,可以用於map的key類型。
+type address struct {
+ hostname string
+ port int
+}
+
+hits := make(map[address]int)
+hits[address{"golang.org", 443}]++
+
TODO
+在本節中,我們將看到如果使用Go語言提供的不同尋常的結構體嵌入機製讓一個命名的結構體包含另一個結構體類型的匿名成員,這樣就可以通過簡單的點運算符x.f來訪問匿名成員鏈中嵌套的x.d.e.f成員。
+考慮一個二維的繪圖程序,提供了一個各種圖形的庫,例如矩形、橢圓形、星形和輪形等幾何形狀。這里是其中兩個的定義:
+type Circle struct {
+ X, Y, Radius int
+}
+
+type Wheel struct {
+ X, Y, Radius, Spokes int
+}
+
+一個Circle代表的圓形類型包含了標準圓心的X和Y坐標信息,和一個Radius表示的半徑信息。一個Wheel輪形除了包含Circle類型所有的全部成員外,還增加了Spokes表示徑向輻條的數量。我們可以這樣創建一個wheel變量:
+var w Wheel
+w.X = 8
+w.Y = 8
+w.Radius = 5
+w.Spokes = 20
+
+隨着庫中幾何形狀數量的增多,我們一定會註意到它們之間的相似和重複之處,所以我們可能爲了便於維護而將相同的屬性獨立出來:
+type Point struct {
+ X, Y int
+}
+
+type Circle struct {
+ Center Point
+ Radius int
+}
+
+type Wheel struct {
+ Circle Circle
+ Spokes int
+}
+
+這樣改動之後結構體類型變的清晰了,但是這種脩改同時也導致了訪問每個成員變得繁瑣:
+var w Wheel
+w.Circle.Center.X = 8
+w.Circle.Center.Y = 8
+w.Circle.Radius = 5
+w.Spokes = 20
+
+Go語言有一個特性讓我們隻聲明一個成員對應的數據類型而不指名成員的名字;這類成員就叫匿名成員。匿名成員的數據類型必鬚是命名的類型或指向一個命名的類型的指針。下面的代碼中,Circle和Wheel各自都有一個匿名成員。我們可以説Point類型被嵌入了Circle結構體,同時Circle類型被嵌入了Wheel結構體。
+type Circle struct {
+ Point
+ Radius int
+}
+
+type Wheel struct {
+ Circle
+ Spokes int
+}
+
+得意於匿名嵌入的特性,我們可以直接訪問葉子屬性而不需要給出完整的路徑:
+var w Wheel
+w.X = 8 // equivalent to w.Circle.Point.X = 8
+w.Y = 8 // equivalent to w.Circle.Point.Y = 8
+w.Radius = 5 // equivalent to w.Circle.Radius = 5
+w.Spokes = 20
+
+在右邊的註釋中給出的顯式形式訪問這些葉子成員的語法依然有效,因此匿名成員併不是眞的無法訪問了。其中匿名成員Circle和Point都有自己的名字——就是命名的類型名字——但是這些名字在點操作符中是可選的。我們在訪問子成員的時候可以忽略任何匿名成員部分。
+不幸的是,結構體字面值併沒有簡短表示匿名成員的語法, 因此下面的語句都不能編譯通過:
+w = Wheel{8, 8, 5, 20} // compile error: unknown fields
+w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
+
+結構體字面值必鬚遵循形狀類型聲明時的結構,所以我們隻能用下面的兩種語法,它們彼此是等價的:
+gopl.io/ch4/embed
+
+w = Wheel{Circle{Point{8, 8}, 5}, 20}
+
+w = Wheel{
+ Circle: Circle{
+ Point: Point{X: 8, Y: 8},
+ Radius: 5,
+ },
+ Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
+}
+
+fmt.Printf("%#v\n", w)
+// Output:
+// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
+
+w.X = 42
+
+fmt.Printf("%#v\n", w)
+// Output:
+// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
+
+需要註意的是Printf函數中%v參數包含的#副詞,它表示用和Go語言類似的語法打印值。對於結構體類型來説,將包含每個成員的名字。
+因爲匿名成員也有一個隱式的名字,因此不能同時包含兩個類型相同的匿名成員,這會導致名字衝突。同時,因爲成員的名字是由其類型隱式地決定的,所有匿名成員也有可見性的規則約束。在上面的例子中,Point和Circle匿名成員都是導出的。卽使它們不導出(比如改成小寫字母開頭的point和circle),我們依然可以用簡短形式訪問匿名成員嵌套的成員
+w.X = 8 // equivalent to w.circle.point.X = 8
+
+但是在包外部,因爲circle和point沒有導出不能訪問它們的成員,因此簡短語法也是禁止的。
+到目前未知,我們看到匿名成員特性隻是對訪問嵌套成員的點運算符提供了簡短的語法醣。稍後,我們將會看到匿名成員併不要求是結構體類型;其實任何命令的類型都可以作爲結構體的匿名成員。但是爲什麽要嵌入一個沒有任何子成員類型的匿名成員類型呢?
+答案是匿名類型的方法集。簡短的點運算符語法可以用於選擇匿名成員嵌套的成員,也可以用於訪問它們的方法。實際上,外層的結構體不僅僅是獲得了匿名成員類型的所有成員,而且也獲得了該類型導出的全部的方法。這個機製可以用於將一個有簡單行爲的對象組合成有複雜行爲的對象。組合是Go語言中面向對象編程的覈心,我們將在6.3節中專門討論。
TODO
+JavaScript對象表示法(JSON)是一種用於發送和接收結構化信息的標準協議。JSON併不是唯一標準協議。 XML(§7.14)、ASN.1和Google的Protocol Buffers都是類似的協議,併且有各自的特色,但是由於簡潔性、可讀性和流行程度等原因,JSON是應用最廣泛的一個。
+Go語言對於這些標準格式的編碼和解碼都有良好的支持,由標準庫中的encoding/json、encoding/xml、encoding/asn1等包提供(譯註:Protocol Buffers的支持由 github.com/golang/protobuf 包提供),併且這類包都有着相似的API接口。本節,我們將對重要的encoding/json包的用法做個概述。
+JSON是對JavaScript中各種值——字符串、數字、布爾值和對象——Unicode本文編碼。它可以用有效可讀的方式表示第三章的基礎數據類型和本章的數組、slice、結構體和map等聚合數據類型。
+基本的JSON類型有數字(十進製或科學記數法)、布爾值(true或false)、字符串,其中字符串是以雙引號包含的Unicode字符序列,支持和Go語言類似的反斜槓轉義特性,不過JSON使用的是\Uhhhh轉義數字來表示一個UTF-16編碼,而不是Go語言的rune類型。
+這些基礎類型可以通過JSON的數組和對象類型進行遞歸組合。一個JSON數組是一個有序的值序列,寫在一個方括號中併以逗號分隔;一個JSON數組可以用於編碼Go語言的數組和slice。一個JSON對象是一個字符串到值的映射,寫成以繫列的name:value對形式,用花括號包含併以逗號分隔;JSON的對象類型可以用於編碼Go語言的map類型(key類型是字符串)和結構體。例如:
+boolean true
+number -273.15
+string "She said \"Hello, BF\""
+array ["gold", "silver", "bronze"]
+object {"year": 1980,
+ "event": "archery",
+ "medals": ["gold", "silver", "bronze"]}
+
考慮一個應用程序,該程序負責收集各種電影評論併提供反饋功能。它的Movie數據類型和一個典型的表示電影的值列表如下所示。(其中結構體聲明中,Year和Color成員後面的字符串面值是結構體成員Tag;我們稍後會解釋它的作用。)
+gopl.io/ch4/movie
+
+type Movie struct {
+ Title string
+ Year int `json:"released"`
+ Color bool `json:"color,omitempty"`
+ Actors []string
+}
+
+var movies = []Movie{
+ {Title: "Casablanca", Year: 1942, Color: false,
+ Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
+ {Title: "Cool Hand Luke", Year: 1967, Color: true,
+ Actors: []string{"Paul Newman"}},
+ {Title: "Bullitt", Year: 1968, Color: true,
+ Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
+ // ...
+}
+
+這樣的數據結構特别適合JSON格式,併且在兩種之間相互轉換也很容易。將一個Go語言中類似movies的結構體slice轉爲JSON的過程叫編組(marshaling)。編組通過調用json.Marshal函數完成:
+data, err := json.Marshal(movies)
+if err != nil {
+ log.Fatalf("JSON marshaling failed: %s", err)
+}
+fmt.Printf("%s\n", data)
+
+Marshal函數生成一個字節slice,包含很長的字符串,併且沒有空白縮進;我們將它摺行以便於顯示:
+[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
+id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac
+tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
+Actors":["Steve McQueen","Jacqueline Bisset"]}]
+
這種緊湊的表示形式雖然包含了全部的信息,但是很難閲讀。爲了生成便於閲讀的格式,另一個json.MarshalIndent函數將産生整齊縮進的輸出。有兩個額外的字符串參數用於表示每一行輸出的前綴和每一個層級的縮進:
+data, err := json.MarshalIndent(movies, "", " ")
+if err != nil {
+ log.Fatalf("JSON marshaling failed: %s", err)
+}
+fmt.Printf("%s\n", data)
+
+上面的代碼將産生這樣的輸出:
+[
+ {
+ "Title": "Casablanca",
+ "released": 1942,
+ "Actors": [
+ "Humphrey Bogart",
+ "Ingrid Bergman"
+ ]
+ },
+ {
+ "Title": "Cool Hand Luke",
+ "released": 1967,
+ "color": true,
+ "Actors": [
+ "Paul Newman"
+ ]
+ },
+ {
+ "Title": "Bullitt",
+ "released": 1968,
+ "color": true,
+ "Actors": [
+ "Steve McQueen",
+ "Jacqueline Bisset"
+ ]
+ }
+]
+
+在編碼時,默認使用Go語言結構體的成員名字作爲JSON的對象(通過reflect反射技術,我們將在12.6節討論)。隻有導出的結構體成員才會被編碼,這也就是我們爲什麽選擇用大寫字母開頭的成員名稱。
+細心的讀者可能已經註意到,其中Year名字的成員在編碼後變成了released,還有Color長遠編碼後變成了小寫字母開頭的color。這是因爲構體成員Tag所導致的。一個構體成員Tag是和在編譯階段關聯到該成員的元信息字符串:
+Year int `json:"released"`
+Color bool `json:"color,omitempty"`
+
結構體的成員Tag可以是任意的字符串面值,但是通常是一繫列用空格分隔的key:"value"鍵值對序列;因爲值中含義雙引號字符,因此成員Tag一般用原生字符串面值的形式書寫。json開頭鍵對應的值用於控製encoding/json包的編碼和解碼的行爲,併且encoding/...下面其它的包也遵循這個約定。成員Tag中json對應值的第一部分用於指定JSON對象的名字,比如將Go語言中的TotalCount成員對應到JSON中的total_count對象。Color成員的Tag還帶了一個額外的omitempty選項,表示當Go語言結構體成員爲空或零值時不生成JSON對象(這里false爲零值)。果然,Casablanca是一個黑白電影,併沒有輸出Color成員。
+編碼的逆操作是解碼,對應將JSON數據解碼爲GO語言的數據結構,Go語言中一般叫unmarshaling,通過json.Unmarshal函數完成。下面的代碼將JSON格式的電影數據解碼爲一個結構體slice,結構體中隻有Title成員。通過定義合適的Go語言數據結構,我們可以選擇性地解碼JSON中感興趣的成員。當Unmarshal返迴,slice將被隻含有Title信息值填充,其它JSON成員將被忽略。
+var titles []struct{ Title string }
+if err := json.Unmarshal(data, &titles); err != nil {
+ log.Fatalf("JSON unmarshaling failed: %s", err)
+}
+fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
+
+許多web服務都提供JSON接口,通過HTTP接口發送JSON格式請求併返迴JSON格式的信息。爲了説明這一點,我們通過Github的issue査詢服務。首先,我們要定義合適的類型和常量:
+gopl.io/ch4/github
+// Package github provides a Go API for the GitHub issue tracker.
+// See https://developer.github.com/v3/search/#search-issues.
+package github
+
+import "time"
+
+const IssuesURL = "https://api.github.com/search/issues"
+
+type IssuesSearchResult struct {
+ TotalCount int `json:"total_count"`
+ Items []*Issue
+}
+
+type Issue struct {
+ Number int
+ HTMLURL string `json:"html_url"`
+ Title string
+ State string
+ User *User
+ CreatedAt time.Time `json:"created_at"`
+ Body string // in Markdown format
+}
+
+type User struct {
+ Login string
+ HTMLURL string `json:"html_url"`
+}
+
+和前面一樣,卽使對應的JSON對象名是小寫字母,每個結構體的成員名也是聲明爲大小字母開頭的。因爲有些JSON成員名字和Go結構體成員名字併不相同,因此需要Go語言結構體成員Tag來指定對應的JSON名字。同樣,在解碼的時候也需要做同樣的處理,GitHub服務返迴的信息比我們定義的要多很多。
+SearchIssues函數發出一個HTTP請求,然後解碼返迴的JSON格式的結果。因爲用戶提供的査詢條件可能包含類似?
和&
之類的特殊字符,爲了避免對URL造成衝突,我們用url.QueryEscape來對査詢中的特殊字符進行轉義操作。
gopl.io/ch4/github
+package github
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// SearchIssues queries the GitHub issue tracker.
+func SearchIssues(terms []string) (*IssuesSearchResult, error) {
+ q := url.QueryEscape(strings.Join(terms, " "))
+ resp, err := http.Get(IssuesURL + "?q=" + q)
+ if err != nil {
+ return nil, err
+ }
+
+ // We must close resp.Body on all execution paths.
+ // (Chapter 5 presents 'defer', which makes this simpler.)
+ if resp.StatusCode != http.StatusOK {
+ resp.Body.Close()
+ return nil, fmt.Errorf("search query failed: %s", resp.Status)
+ }
+
+ var result IssuesSearchResult
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ resp.Body.Close()
+ return nil, err
+ }
+ resp.Body.Close()
+ return &result, nil
+}
+
+在早些的例子中,我們使用了json.Unmarshal函數來將JSON格式的字符串解碼爲字節slice。但是這個例子中,我們使用了基於流式的解碼器json.Decoder,它可以從一個流解碼JSON數據,盡管這不是必鬚的。如您所料,還有一個針對輸出流的json.Encoder編碼對象。
+我們調用Decode方法來填充變量。這里有多種方法可以格式化結構。下面是最簡單的一種,以一個固定寬度打印每個issue,但是在下一節我們將看到如果利用模闆來輸出複雜的格式。
+gopl.io/ch4/issues
+
+// Issues prints a table of GitHub issues matching the search terms.
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+
+ "gopl.io/ch4/github"
+)
+
+func main() {
+ result, err := github.SearchIssues(os.Args[1:])
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%d issues:\n", result.TotalCount)
+ for _, item := range result.Items {
+ fmt.Printf("#%-5d %9.9s %.55s\n",
+ item.Number, item.User.Login, item.Title)
+ }
+}
+
+通過命令行參數指定檢索條件。下面的命令是査詢Go語言項目中和JSON解碼相關的問題,還有査詢返迴的結果:
+$ go build gopl.io/ch4/issues
+$ ./issues repo:golang/go is:open json decoder
+13 issues:
+#5680 eaigner encoding/json: set key converter on en/decoder
+#6050 gopherbot encoding/json: provide tokenizer
+#8658 gopherbot encoding/json: use bufio
+#8462 kortschak encoding/json: UnmarshalText confuses json.Unmarshal
+#5901 rsc encoding/json: allow override type marshaling
+#9812 klauspost encoding/json: string tag not symmetric
+#7872 extempora encoding/json: Encoder internally buffers full output
+#9650 cespare encoding/json: Decoding gives errPhase when unmarshalin
+#6716 gopherbot encoding/json: include field name in unmarshal error me
+#6901 lukescott encoding/json, encoding/xml: option to treat unknown fi
+#6384 joeshaw encoding/json: encode precise floating point integers u
+#6647 btracey x/tools/cmd/godoc: display type kind of each named type
+#4237 gjemiller encoding/base64: URLEncoding padding is optional
+
GitHub的Web服務接口 https://developer.github.com/v3/ 包含了更多的特性。
+練習 4.10: 脩改issues程序,根據問題的時間進行分類,比如不到一個月的、不到一年的、超過一年。
+練習 4.11: 編寫一個工具,允許用戶在命令行創建、讀取、更新和刪除GitHub上的issue,當必要的時候自動打開用戶默認的編輯器用於輸入文本信息。
+練習 4.12: 流行的web漫畵服務xkcd也提供了JSON接口。例如,一個 https://xkcd.com/571/info.0.json 請求將返迴一個很多人喜愛的571編號的詳細描述。下載每個鏈接(隻下載一次)然後創建一個離線索引。編寫一個xkcd工具,使用這些離線索引,打印和命令行輸入的檢索詞相匹配的漫畵的URL。
+練習 4.13: 使用開放電影數據庫的JSON服務接口,允許你檢索和下載 https://omdbapi.com/ 上電影的名字和對應的海報圖像。編寫一個poster工具,通過命令行輸入的電影名字,下載對應的海報。
TODO
+前面的例子,隻是最簡單的格式,使用Printf是完全足夠的。但是有時候會需要複雜的打印格式,這時候一般需要將格式化代碼分離出來以便更安全地脩改。這寫功能是由text/template和html/template等模闆包提供的,它們提供了一個用變量值填充到一個文本或HTML格式的模闆的機製。
+一個模闆是一個字符串或一個文件,里面包含了一個或多個由雙花括號包含的action對象。大部分的字符串隻是按面值打印,但是對於actions部分將觸發其它的行爲。買個actions包好了一個用模闆語言書寫的表達式,一個雖然簡短但是可以輸出複雜的打印值,模闆語言包含通過選擇結構體的成員、調用函數或方法、表達式控製流if-else語句和range循環語句,還有其它實例化模闆等諸多特性。下面是一個簡單的模闆字符串:
+gopl.io/ch4/issuesreport
+
+const templ = `{{.TotalCount}} issues:
+{{range .Items}}----------------------------------------
+Number: {{.Number}}
+User: {{.User.Login}}
+Title: {{.Title | printf "%.64s"}}
+Age: {{.CreatedAt | daysAgo}} days
+{{end}}`
+
+這個模闆先打印匹配到的issue總數,然後打印每個issue的編號、創建用戶、標題還有存在的時間。每一個action,都有一個當前值的概念,對應點操作符,寫作“.”。當前值“.”最初被初始化爲調用模闆是的參數,在當前例子中對應github.IssuesSearchResult類型的變量。模闆中{{.TotalCount}}
對應action將展開爲結構體中TotalCount成員以默認的方式打印的值。模闆中{{range .Items}}
和{{end}}
對應一個循環action,因此它們直接的內容可能會被展開多次,循環每次迭代的當前值對應當前的Items元素的值。
在一個action中,|
操作符表示將前一個表達式的結果作爲後一個函數的輸入,類似於UNIX中管道的概念。在Title這一行的action中,第二個操作是一個printf函數,是一個基於fmt.Sprintf實現的內置函數,所有模闆都可以直接使用。對於Age部分,第二個動作是一個叫daysAgo的函數,通過time.Since函數將CreatedAt成員轉換爲過去的時間長度:
func daysAgo(t time.Time) int {
+ return int(time.Since(t).Hours() / 24)
+}
+
+需要註意的是CreatedAt的參數類型是time.Time,併不是字符串。以同樣的方式,我們可以通過定義一些方法來控製字符串的格式化(§2.5),一個類型同樣可以定製自己的JSON編碼和解碼行爲。time.Time類型對應的JSON值是一個標準時間格式的字符串。
+生成模闆的輸出需要兩個處理步驟。第一步是要分析模闆併轉爲內部表示,然後基於指定的輸入執行模闆。分析模闆部分一般隻需要執行一次。下面的代碼創建併分析上面定義的模闆templ。註意方法調用鏈的順序:template.New先創建併返迴一個模闆;Funcs方法將daysAgo等自定義函數註冊到模闆中,併返迴模闆;最後調用Parse函數分析模闆。
+report, err := template.New("report").
+ Funcs(template.FuncMap{"daysAgo": daysAgo}).
+ Parse(templ)
+if err != nil {
+ log.Fatal(err)
+}
+
+因爲模闆通常在編譯時就測試好了,如果模闆解析失敗將是一個致命的錯誤。template.Must輔助函數可以簡化這個致命錯誤的處理:它接受一個模闆和一個error類型的參數,檢測error是否爲nil(如果不是則發出panic異常),然後返迴傳入的模闆。我們將在5.9節再討論這個話題。
+一旦模闆已經創建、註冊了daysAgo函數、併通過分析和檢測,我們就可以使用github.IssuesSearchResult作爲輸入源、os.Stdout作爲輸出源來執行模闆:
+var report = template.Must(template.New("issuelist").
+ Funcs(template.FuncMap{"daysAgo": daysAgo}).
+ Parse(templ))
+
+func main() {
+ result, err := github.SearchIssues(os.Args[1:])
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := report.Execute(os.Stdout, result); err != nil {
+ log.Fatal(err)
+ }
+}
+
+程序輸出一個純文本報告:
+$ go build gopl.io/ch4/issuesreport
+$ ./issuesreport repo:golang/go is:open json decoder
+13 issues:
+----------------------------------------
+Number: 5680
+User: eaigner
+Title: encoding/json: set key converter on en/decoder
+Age: 750 days
+----------------------------------------
+Number: 6050
+User: gopherbot
+Title: encoding/json: provide tokenizer
+Age: 695 days
+----------------------------------------
+...
+
現在讓我們轉到html/template模闆包。它使用和text/template包相同的API和模闆語言,但是增加了一個將字符串自動轉義,以避免輸入字符串和HTML、JavaScript、CSS或URL語法産生衝突的問題。這個特性可以避免一些長期存在的安全問題,比如通過生成HTML註入攻擊,通過構造一個含有惡意代碼的問題標題,這些都可能讓模闆輸出錯誤的輸出,從而讓他們控製頁面。
+下面的模闆以HTML格式輸出issue列表。註意import語句的不同:
+gopl.io/ch4/issueshtml
+
+import "html/template"
+
+var issueList = template.Must(template.New("issuelist").Parse(`
+<h1>{{.TotalCount}} issues</h1>
+<table>
+<tr style='text-align: left'>
+ <th>#</th>
+ <th>State</th>
+ <th>User</th>
+ <th>Title</th>
+</tr>
+{{range .Items}}
+<tr>
+ <td><a href='{{.HTMLURL}}'>{{.Number}}</td>
+ <td>{{.State}}</td>
+ <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
+ <td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
+</tr>
+{{end}}
+</table>
+`))
+
+下面的命令將在新的模闆上執行一個稍微不同的査詢:
+$ go build gopl.io/ch4/issueshtml
+$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html
+
+圖4.4顯示了在web瀏覽器中的效果圖。每個issue包含到Github對應頁面的鏈接。
+圖4.4中的沒有問題會對HTML格式産生衝突,但是我們馬上將看到標題中含有&
和<
字符的issue。下面的命令選擇了兩個這樣的issue:
$ ./issueshtml repo:golang/go 3133 10535 >issues2.html
+
圖4.5顯示了該査詢的結果。註意,html/template包已經自動將特殊字符轉義,我們依然可以看到正確的字面值。如果我們使用text/template包的話,這2個issue將會産生錯誤,其中“<”四個字符將會被當作小於字符“<”處理,同時“”字符串將會被當作一個鏈接元素處理,它們都會導致HTML文檔結構的改變,從而導致有未知的風險。
+我們也可以通過對信任的HTML字符串使用template.HTML類型來抑製這種自動轉義的行爲。還有很多采用類型命名的字符串類型對應信任的JavaScript、CSS和URL。下面的程序演示了兩個使用不同類型的相同字符串産生的不同結果:A是一個普通字符串,B是一個信任的template.HTML字符串類型。
+gopl.io/ch4/autoescape
+
+func main() {
+ const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
+ t := template.Must(template.New("escape").Parse(templ))
+ var data struct {
+ A string // untrusted plain text
+ B template.HTML // trusted HTML
+ }
+ data.A = "<b>Hello!</b>"
+ data.B = "<b>Hello!</b>"
+ if err := t.Execute(os.Stdout, data); err != nil {
+ log.Fatal(err)
+ }
+}
+
+圖4.6顯示了出現在瀏覽器中的模闆輸出。我們看到A的黑體標記被轉義失效了,但是B沒有。
+我們這里隻講述了模闆繫統中最基本的特性。一如旣往,如果想了解更多的信息,請自己査看包文檔:
+$ go doc text/template
+$ go doc html/template
+
練習 4.14: 創建一個web服務器,査詢一次GitHub,然後生成BUG報告、里程碑和對應的用戶信息。
TODO
+clock服務器每一個連接都會起一個goroutine。在本節中我們會創建一個echo服務器,這個服務在每個連接中會有多個goroutine。大多數echo服務僅僅會返迴他們讀取到的內容,就像下面這個簡單的handleConn函數所做的一樣:
+func handleConn(c net.Conn) {
+ io.Copy(c, c) // NOTE: ignoring errors
+ c.Close()
+}
+
+一個更有意思的echo服務應該模擬一個實際的echo的“迴響”,併且一開始要用大寫HELLO來表示“聲音很大”,之後經過一小段延遲返迴一個有所緩和的Hello,然後一個全小寫字母的hello表示聲音漸漸變小直至消失,像下面這個版本的handleConn(譯註:笑看作者腦洞大開):
+gopl.io/ch8/reverb1
+func echo(c net.Conn, shout string, delay time.Duration) {
+ fmt.Fprintln(c, "\t", strings.ToUpper(shout))
+ time.Sleep(delay)
+ fmt.Fprintln(c, "\t", shout)
+ time.Sleep(delay)
+ fmt.Fprintln(c, "\t", strings.ToLower(shout))
+}
+
+func handleConn(c net.Conn) {
+ input := bufio.NewScanner(c)
+ for input.Scan() {
+ echo(c, input.Text(), 1*time.Second)
+ }
+ // NOTE: ignoring potential errors from input.Err()
+ c.Close()
+}
+
+我們需要陞級我們的客戶端程序,這樣它就可以發送終端的輸入到服務器,併把服務端的返迴輸出到終端上,這使我們有了使用併發的另一個好機會:
+gopl.io/ch8/netcat2
+func main() {
+ conn, err := net.Dial("tcp", "localhost:8000")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer conn.Close()
+ go mustCopy(os.Stdout, conn)
+ mustCopy(conn, os.Stdin)
+}
+
+當main goroutine從標準輸入流中讀取內容併將其發送給服務器時,另一個goroutine會讀取併打印服務端的響應。當main goroutine碰到輸入終止時,例如,用戶在終端中按了Control-D(^D),在windows上是Control-Z,這時程序就會被終止,盡管其它goroutine中還有進行中的任務。(在8.4.1中引入了channels後我們會明白如何讓程序等待兩邊都結束)。
+下面這個會話中,客戶端的輸入是左對齊的,服務端的響應會用縮進來區别顯示。 +客戶端會向服務器“喊三次話”:
+$ go build gopl.io/ch8/reverb1
+$ ./reverb1 &
+$ go build gopl.io/ch8/netcat2
+$ ./netcat2
+Hello?
+ HELLO?
+ Hello?
+ hello?
+Is there anybody there?
+ IS THERE ANYBODY THERE?
+Yooo-hooo!
+ Is there anybody there?
+ is there anybody there?
+ YOOO-HOOO!
+ Yooo-hooo!
+yooo-hooo!
+^D
+$ killall reverb1
+
註意客戶端的第三次shout在前一個shout處理完成之前一直沒有被處理,這貌似看起來不是特别“現實”。眞實世界里的迴響應該是會由三次shout的迴聲組合而成的。爲了模擬眞實世界的迴響,我們需要更多的goroutine來做這件事情。這樣我們就再一次地需要go這個關鍵詞了,這次我們用它來調用echo:
+gopl.io/ch8/reverb2
+func handleConn(c net.Conn) {
+ input := bufio.NewScanner(c)
+ for input.Scan() {
+ go echo(c, input.Text(), 1*time.Second)
+ }
+ // NOTE: ignoring potential errors from input.Err()
+ c.Close()
+}
+
+go後跟的函數的參數會在go語句自身執行時被求值;因此input.Text()會在main goroutine中被求值。 +現在迴響是併發併且會按時間來覆蓋掉其它響應了:
+$ go build gopl.io/ch8/reverb2
+$ ./reverb2 &
+$ ./netcat2
+Is there anybody there?
+ IS THERE ANYBODY THERE?
+Yooo-hooo!
+ Is there anybody there?
+ YOOO-HOOO!
+ is there anybody there?
+ Yooo-hooo!
+ yooo-hooo!
+^D
+$ killall reverb2
+
讓服務使用併發不隻是處理多個客戶端的請求,甚至在處理單個連接時也可能會用到,就像我們上面的兩個go關鍵詞的用法。然而在我們使用go關鍵詞的同時,需要慎重地考慮net.Conn中的方法在併發地調用時是否安全,事實上對於大多數類型來説也確實不安全。我們會在下一章中詳細地探討併發安全性。
TODO
+下面的程序會進行火箭發射的倒計時。time.Tick函數返迴一個channel,程序會週期性地像一個節拍器一樣向這個channel發送事件。每一個事件的值是一個時間戳,不過更有意思的是其傳送方式。
+gopl.io/ch8/countdown1
+func main() {
+ fmt.Println("Commencing countdown.")
+ tick := time.Tick(1 * time.Second)
+ for countdown := 10; countdown > 0; countdown-- {
+ fmt.Println(countdown)
+ j<-tick
+ }
+ launch()
+}
+
+現在我們讓這個程序支持在倒計時中,用戶按下return鍵時直接中斷發射流程。首先,我們啟動一個goroutine,這個goroutine會嚐試從標準輸入中調入一個單獨的byte併且,如果成功了,會向名爲abort的channel發送一個值。
+gopl.io/ch8/countdown2
+abort := make(chan struct{})
+go func() {
+ os.Stdin.Read(make([]byte, 1)) // read a single byte
+ abort <- struct{}{}
+}()
+
+現在每一次計數循環的迭代都需要等待兩個channel中的其中一個返迴事件了:ticker channel當一切正常時(就像NASA jorgon的"nominal",譯註:這梗估計我們是不懂了)或者異常時返迴的abort事件。我們無法做到從每一個channel中接收信息,如果我們這麽做的話,如果第一個channel中沒有事件發過來那麽程序就會立刻被阻塞,這樣我們就無法收到第二個channel中發過來的事件。這時候我們需要多路複用(multiplex)這些操作了,爲了能夠多路複用,我們使用了select語句。
+select {
+case <-ch1:
+ // ...
+case x := <-ch2:
+ // ...use x...
+case ch3 <- y:
+ // ...
+default:
+ // ...
+}
+
+上面是select語句的一般形式。和switch語句稍微有點相似,也會有幾個case和最後的default選擇支。每一個case代表一個通信操作(在某個channel上進行發送或者接收)併且會包含一些語句組成的一個語句塊。一個接收表達式可能隻包含接收表達式自身(譯註:不把接收到的值賦值給變量什麽的),就像上面的第一個case,或者包含在一個簡短的變量聲明中,像第二個case里一樣;第二種形式讓你能夠引用接收到的值。
+select會等待case中有能夠執行的case時去執行。當條件滿足時,select才會去通信併執行case之後的語句;這時候其它通信是不會執行的。一個沒有任何case的select語句寫作select{},會永遠地等待下去。
+讓我們迴到我們的火箭發射程序。time.After函數會立卽返迴一個channel,併起一個新的goroutine在經過特定的時間後向該channel發送一個獨立的值。下面的select語句會會一直等待到兩個事件中的一個到達,無論是abort事件或者一個10秒經過的事件。如果10秒經過了還沒有abort事件進入,那麽火箭就會發射。
+func main() {
+ // ...create abort channel...
+
+ fmt.Println("Commencing countdown. Press return to abort.")
+ select {
+ case <-time.After(10 * time.Second):
+ // Do nothing.
+ case <-abort:
+ fmt.Println("Launch aborted!")
+ return
+ }
+ launch()
+}
+
+下面這個例子更微秒。ch這個channel的buffer大小是1,所以會交替的爲空或爲滿,所以隻有一個case可以進行下去,無論i是奇數或者偶數,它都會打印0 2 4 6 8。
+ch := make(chan int, 1)
+for i := 0; i < 10; i++ {
+ select {
+ case x := <-ch:
+ fmt.Println(x) // "0" "2" "4" "6" "8"
+ case ch <- i:
+ }
+}
+
+如果多個case同時就緒時,select會隨機地選擇一個執行,這樣來保證每一個channel都有平等的被select的機會。增加前一個例子的buffer大小會使其輸出變得不確定,因爲當buffer旣不爲滿也不爲空時,select語句的執行情況就像是拋硬幣的行爲一樣是隨機的。
+下面讓我們的發射程序打印倒計時。這里的select語句會使每次循環迭代等待一秒來執行退出操作。
+gopl.io/ch8/countdown3
+func main() {
+ // ...create abort channel...
+
+ fmt.Println("Commencing countdown. Press return to abort.")
+ tick := time.Tick(1 * time.Second)
+ for countdown := 10; countdown > 0; countdown-- {
+ fmt.Println(countdown)
+ select {
+ case <-tick:
+ // Do nothing.
+ case <-abort:
+ fmt.Println("Launch aborted!")
+ return
+ }
+ }
+ launch()
+}
+
+time.Tick函數表現得好像它創建了一個在循環中調用time.Sleep的goroutine,每次被喚醒時發送一個事件。當countdown函數返迴時,它會停止從tick中接收事件,但是ticker這個goroutine還依然存活,繼續徒勞地嚐試從channel中發送值,然而這時候已經沒有其它的goroutine會從該channel中接收值了--這被稱爲goroutine洩露(§8.4.4)。
+Tick函數挺方便,但是隻有當程序整個生命週期都需要這個時間時我們使用它才比較合適。否則的話,我們應該使用下面的這種模式:
+ticker := time.NewTicker(1 * time.Second)
+<-ticker.C // receive from the ticker's channel
+ticker.Stop() // cause the ticker's goroutine to terminate
+
+有時候我們希望能夠從channel中發送或者接收值,併避免因爲發送或者接收導致的阻塞,尤其是當channel沒有準備好寫或者讀時。select語句就可以實現這樣的功能。select會有一個default來設置當其它的操作都不能夠馬上被處理時程序需要執行哪些邏輯。
+下面的select語句會在abort channel中有值時,從其中接收值;無值時什麽都不做。這是一個非阻塞的接收操作;反複地做這樣的操作叫做“輪詢channel”。
+select {
+case <-abort:
+ fmt.Printf("Launch aborted!\n")
+ return
+default:
+ // do nothing
+}
+
+channel的零值是nil。也許會讓你覺得比較奇怪,nil的channel有時候也是有一些用處的。因爲對一個nil的channel發送和接收操作會永遠阻塞,在select語句中操作nil的channel永遠都不會被select到。
+這使得我們可以用nil來激活或者禁用case,來達成處理其它輸入或輸出事件時超時和取消的邏輯。我們會在下一節中看到一個例子。
+練習8.8: 使用select來改造8.3節中的echo服務器,爲其增加超時,這樣服務器可以在客戶端10秒中沒有任何喊話時自動斷開連接。
Go語言聖經 《The Go Programming Language》 中文版本,僅供編程和英語學習交流之用,請在下載後24小時內刪除。
+Go語言聖經 《The Go Programming Language》 中文版本,僅供學習交流之用。