Golang 需要避免踩的 50 個坑(三)
最近準備寫一些關於golang的技術博文,本文是之前在GitHub上看到的golang技術譯文,感覺很有幫助,先給各位讀者分享一下。
前言
Go 是一門簡單有趣的程式語言,與其他語言一樣,在使用時不免會遇到很多坑,不過它們大多不是 Go 本身的設計缺陷。如果你剛從其他語言轉到 Go,那這篇文章裡的坑多半會踩到。
如果花時間學習官方 doc、wiki、討論郵件列表 、Rob Pike 的大量文章以及 Go 的原始碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減少大量除錯程式碼的時間。
中級篇:35-50
35. 關閉 HTTP 的響應體
使用 HTTP 標準庫發起請求、獲取響應時,即使你不從響應中讀取任何資料或響應為空,都需要手動關閉響應體。新手很容易忘記手動關閉,或者寫在了錯誤的位置:
// 請求失敗造成 panic func main() { resp, err := http.Get("https://api.ipify.org?format=json") defer resp.Body.Close()// resp 可能為 nil,不能讀取 Body if err != nil { fmt.Println(err) return } body, err := ioutil.ReadAll(resp.Body) checkError(err) fmt.Println(string(body)) } func checkError(err error) { if err != nil{ log.Fatalln(err) } }
上邊的程式碼能正確發起請求,但是一旦請求失敗,變數resp
值為nil
,造成 panic:
panic: runtime error: invalid memory address or nil pointer dereference
應該先檢查 HTTP 響應錯誤為nil
,再呼叫resp.Body.Close()
來關閉響應體:
// 大多數情況正確的示例 func main() { resp, err := http.Get("https://api.ipify.org?format=json") checkError(err) defer resp.Body.Close()// 絕大多數情況下的正確關閉方式 body, err := ioutil.ReadAll(resp.Body) checkError(err) fmt.Println(string(body)) }
輸出:
Gethttps://api.ipify.org?format=json : x509: certificate signed by unknown authority
絕大多數請求失敗的情況下,resp
的值為nil
且err
為non-nil
。但如果你得到的是重定向錯誤,那它倆的值都是non-nil
,最後依舊可能發生記憶體洩露。2 個解決辦法:
defer
// 正確示例 func main() { resp, err := http.Get("http://www.baidu.com") // 關閉 resp.Body 的正確姿勢 if resp != nil { defer resp.Body.Close() } checkError(err) body, err := ioutil.ReadAll(resp.Body) checkError(err) fmt.Println(string(body)) }
resp.Body.Close()
早先版本的實現是讀取響應體的資料之後丟棄,保證了 keep-alive 的 HTTP 連線能重用處理不止一個請求。但 Go 的最新版本將讀取並丟棄資料的任務交給了使用者,如果你不處理,HTTP 連線可能會直接關閉而非重用,參考在 Go 1.5 版本文件。
如果程式大量重用 HTTP 長連線,你可能要在處理響應的邏輯程式碼中加入:
_, err = io.Copy(ioutil.Discard, resp.Body)// 手動丟棄讀取完畢的資料
如果你需要完整讀取響應,上邊的程式碼是需要寫的。比如在解碼 API 的 JSON 響應資料:
json.NewDecoder(resp.Body).Decode(&data)
36. 關閉 HTTP 連線
一些支援 HTTP1.1 或 HTTP1.0 配置了connection: keep-alive
選項的伺服器會保持一段時間的長連線。但標準庫 “net/http” 的連線預設只在伺服器主動要求關閉時才斷開,所以你的程式可能會消耗完 socket 描述符。解決辦法有 2 個,請求結束後:
-
直接設定請求變數的
Close
欄位值為true
,每次請求結束後就會主動關閉連線。 -
設定 Header 請求頭部選項
Connection: close
,然後伺服器返回的響應頭部也會有這個選項,此時 HTTP 標準庫會主動斷開連線。
// 主動關閉連線 func main() { req, err := http.NewRequest("GET", "http://golang.org", nil) checkError(err) req.Close = true //req.Header.Add("Connection", "close")// 等效的關閉方式 resp, err := http.DefaultClient.Do(req) if resp != nil { defer resp.Body.Close() } checkError(err) body, err := ioutil.ReadAll(resp.Body) checkError(err) fmt.Println(string(body)) }
你可以建立一個自定義配置的 HTTP transport 客戶端,用來取消 HTTP 全域性的複用連線:
func main() { tr := http.Transport{DisableKeepAlives: true} client := http.Client{Transport: &tr} resp, err := client.Get("https://golang.google.cn/") if resp != nil { defer resp.Body.Close() } checkError(err) fmt.Println(resp.StatusCode)// 200 body, err := ioutil.ReadAll(resp.Body) checkError(err) fmt.Println(len(string(body))) }
根據需求選擇使用場景:
- 若你的程式要向同一伺服器發大量請求,使用預設的保持長連線。
-
若你的程式要連線大量的伺服器,且每臺伺服器只請求一兩次,那收到請求後直接關閉連線。或增加最大檔案開啟數
fs.file-max
的值。
37. 將 JSON 中的數字解碼為 interface 型別
在 encode/decode JSON 資料時,Go 預設會將數值當做 float64 處理,比如下邊的程式碼會造成 panic:
func main() { var data = []byte(`{"status": 200}`) var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { log.Fatalln(err) } fmt.Printf("%T\n", result["status"])// float64 var status = result["status"].(int)// 型別斷言錯誤 fmt.Println("Status value: ", status) }
panic: interface conversion: interface {} is float64, not int
如果你嘗試 decode 的 JSON 欄位是整型,你可以:
-
將 int 值轉為 float 統一使用
-
將 decode 後需要的 float 值轉為 int 使用
// 將 decode 的值轉為 int 使用 func main() { var data = []byte(`{"status": 200}`) var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { log.Fatalln(err) } var status = uint64(result["status"].(float64)) fmt.Println("Status value: ", status) }
-
使用
Decoder
型別來 decode JSON 資料,明確表示欄位的值型別
// 指定欄位型別 func main() { var data = []byte(`{"status": 200}`) var result map[string]interface{} var decoder = json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() if err := decoder.Decode(&result); err != nil { log.Fatalln(err) } var status, _ = result["status"].(json.Number).Int64() fmt.Println("Status value: ", status) } // 你可以使用 string 來儲存數值資料,在 decode 時再決定按 int 還是 float 使用 // 將資料轉為 decode 為 string func main() { var data = []byte({"status": 200}) var result map[string]interface{} var decoder = json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() if err := decoder.Decode(&result); err != nil { log.Fatalln(err) } var status uint64 err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status); checkError(err) fmt.Println("Status value: ", status) }
- 使用struct
型別將你需要的資料對映為數值型
// struct 中指定欄位型別 func main() { var data = []byte(`{"status": 200}`) var result struct { Status uint64 `json:"status"` } err := json.NewDecoder(bytes.NewReader(data)).Decode(&result) checkError(err) fmt.Printf("Result: %+v", result) }
-
可以使用
struct
將數值型別對映為json.RawMessage
原生資料型別適用於如果 JSON 資料不著急 decode 或 JSON 某個欄位的值型別不固定等情況:
// 狀態名稱可能是 int 也可能是 string,指定為 json.RawMessage 型別 func main() { records := [][]byte{ []byte(`{"status":200, "tag":"one"}`), []byte(`{"status":"ok", "tag":"two"}`), } for idx, record := range records { var result struct { StatusCode uint64 StatusName string Statusjson.RawMessage `json:"status"` Tagstring`json:"tag"` } err := json.NewDecoder(bytes.NewReader(record)).Decode(&result) checkError(err) var name string err = json.Unmarshal(result.Status, &name) if err == nil { result.StatusName = name } var code uint64 err = json.Unmarshal(result.Status, &code) if err == nil { result.StatusCode = code } fmt.Printf("[%v] result => %+v\n", idx, result) } }
38. struct、array、slice 和 map 的值比較
可以使用相等運算子==
來比較結構體變數,前提是兩個結構體的成員都是可比較的型別:
type data struct { numint fpfloat32 complex complex64 strstring charrune yesbool events<-chan string handler interface{} ref*byte raw[10]byte } func main() { v1 := data{} v2 := data{} fmt.Println("v1 == v2: ", v1 == v2)// true }
如果兩個結構體中有任意成員是不可比較的,將會造成編譯錯誤。注意陣列成員只有在陣列元素可比較時候才可比較。
type data struct { numint checks [10]func() bool// 無法比較 doItfunc() bool// 無法比較 mmap[string]string// 無法比較 bytes[]byte// 無法比較 } func main() { v1 := data{} v2 := data{} fmt.Println("v1 == v2: ", v1 == v2) }
invalid operation: v1 == v2 (struct containing [10]func() bool cannot be compared)
Go 提供了一些庫函式來比較那些無法使用==
比較的變數,比如使用 “reflect” 包的DeepEqual()
:
// 比較相等運算子無法比較的元素 func main() { v1 := data{} v2 := data{} fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))// true m1 := map[string]string{"one": "a", "two": "b"} m2 := map[string]string{"two": "b", "one": "a"} fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2))// true s1 := []int{1, 2, 3} s2 := []int{1, 2, 3} // 注意兩個 slice 相等,值和順序必須一致 fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2))// true }
這種比較方式可能比較慢,根據你的程式需求來使用。DeepEqual()
還有其他用法:
func main() { var b1 []byte = nil b2 := []byte{} fmt.Println("b1 == b2: ", reflect.DeepEqual(b1, b2))// false }
注意:
-
DeepEqual()
並不總適合於比較 slice
func main() { var str = "one" var in interface{} = "one" fmt.Println("str == in: ", reflect.DeepEqual(str, in))// true v1 := []string{"one", "two"} v2 := []string{"two", "one"} fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2))// false data := map[string]interface{}{ "code":200, "value": []string{"one", "two"}, } encoded, _ := json.Marshal(data) var decoded map[string]interface{} json.Unmarshal(encoded, &decoded) fmt.Println("data == decoded: ", reflect.DeepEqual(data, decoded))// false }
如果要大小寫不敏感來比較 byte 或 string 中的英文文字,可以使用 “bytes” 或 “strings” 包的ToUpper()
和ToLower()
函式。比較其他語言的 byte 或 string,應使用bytes.EqualFold()
和strings.EqualFold()
如果 byte slice 中含有驗證使用者身份的資料(密文雜湊、token 等),不應再使用reflect.DeepEqual()
、bytes.Equal()
、bytes.Compare()
。這三個函式容易對程式造成timing attacks
,此時應使用 “crypto/subtle” 包中的subtle.ConstantTimeCompare()
等函式
-
reflect.DeepEqual()
認為空 slice 與 nil slice 並不相等,但注意byte.Equal()
會認為二者相等:
func main() { var b1 []byte = nil b2 := []byte{} // b1 與 b2 長度相等、有相同的位元組序 // nil 與 slice 在位元組上是相同的 fmt.Println("b1 == b2: ", bytes.Equal(b1, b2))// true }
39. 從 panic 中恢復
在一個 defer 延遲執行的函式中呼叫recover()
,它便能捕捉 / 中斷 panic
// 錯誤的 recover 呼叫示例 func main() { recover()// 什麼都不會捕捉 panic("not good")// 發生 panic,主程式退出 recover()// 不會被執行 println("ok") } // 正確的 recover 呼叫示例 func main() { defer func() { fmt.Println("recovered: ", recover()) }() panic("not good") }
從上邊可以看出,recover()
僅在 defer 執行的函式中呼叫才會生效。
// 錯誤的呼叫示例 func main() { defer func() { doRecover() }() panic("not good") } func doRecover() { fmt.Println("recobered: ", recover()) }
recobered:
40. 在 range 迭代 slice、array、map 時通過更新引用來更新元素
在 range 迭代中,得到的值其實是元素的一份值拷貝,更新拷貝並不會更改原來的元素,即是拷貝的地址並不是原有元素的地址:
func main() { data := []int{1, 2, 3} for _, v := range data { v *= 10// data 中原有元素是不會被修改的 } fmt.Println("data: ", data)// data:[1 2 3] }
如果要修改原有元素的值,應該使用索引直接訪問:
func main() { data := []int{1, 2, 3} for i, v := range data { data[i] = v * 10 } fmt.Println("data: ", data)// data:[10 20 30] }
如果你的集合儲存的是指向值的指標,需稍作修改。依舊需要使用索引訪問元素,不過可以使用 range 出來的元素直接更新原有值:
func main() { data := []*struct{ num int }{{1}, {2}, {3},} for _, v := range data { v.num *= 10// 直接使用指標更新 } fmt.Println(data[0], data[1], data[2])// &{10} &{20} &{30} }
41. slice 中隱藏的資料
從 slice 中重新切出新 slice 時,新 slice 會引用原 slice 的底層陣列。如果跳了這個坑,程式可能會分配大量的臨時 slice 來指向原底層陣列的部分資料,將導致難以預料的記憶體使用。
func get() []byte { raw := make([]byte, 10000) fmt.Println(len(raw), cap(raw), &raw[0])// 10000 10000 0xc420080000 return raw[:3]// 重新分配容量為 10000 的 slice } func main() { data := get() fmt.Println(len(data), cap(data), &data[0])// 3 10000 0xc420080000 }
可以通過拷貝臨時 slice 的資料,而不是重新切片來解決:
func get() (res []byte) { raw := make([]byte, 10000) fmt.Println(len(raw), cap(raw), &raw[0])// 10000 10000 0xc420080000 res = make([]byte, 3) copy(res, raw[:3]) return } func main() { data := get() fmt.Println(len(data), cap(data), &data[0])// 3 3 0xc4200160b8 }
42. Slice 中資料的誤用
舉個簡單例子,重寫檔案路徑(儲存在 slice 中)
分割路徑來指向每個不同級的目錄,修改第一個目錄名再重組子目錄名,建立新路徑:
// 錯誤使用 slice 的拼接示例 func main() { path := []byte("AAAA/BBBBBBBBB") sepIndex := bytes.IndexByte(path, '/') // 4 println(sepIndex) dir1 := path[:sepIndex] dir2 := path[sepIndex+1:] println("dir1: ", string(dir1))// AAAA println("dir2: ", string(dir2))// BBBBBBBBB dir1 = append(dir1, "suffix"...) println("current path: ", string(path))// AAAAsuffixBBBB path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'}) println("dir1: ", string(dir1))// AAAAsuffix println("dir2: ", string(dir2))// uffixBBBB println("new path: ", string(path))// AAAAsuffix/uffixBBBB// 錯誤結果 }
拼接的結果不是正確的AAAAsuffix/BBBBBBBBB
,因為 dir1、 dir2 兩個 slice 引用的資料都是path
的底層陣列,第 13 行修改dir1
同時也修改了path
,也導致了dir2
的修改
解決方法:
input[low:high:max]
// 使用 full slice expression func main() { path := []byte("AAAA/BBBBBBBBB") sepIndex := bytes.IndexByte(path, '/') // 4 dir1 := path[:sepIndex:sepIndex]// 此時 cap(dir1) 指定為4, 而不是先前的 16 dir2 := path[sepIndex+1:] dir1 = append(dir1, "suffix"...) path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'}) println("dir1: ", string(dir1))// AAAAsuffix println("dir2: ", string(dir2))// BBBBBBBBB println("new path: ", string(path))// AAAAsuffix/BBBBBBBBB }
第 6 行中第三個引數是用來控制 dir1 的新容量,再往 dir1 中 append 超額元素時,將分配新的 buffer 來儲存。而不是覆蓋原來的 path 底層陣列
43. 舊 slice
當你從一個已存在的 slice 建立新 slice 時,二者的資料指向相同的底層陣列。如果你的程式使用這個特性,那需要注意 “舊”(stale) slice 問題。
某些情況下,向一個 slice 中追加元素而它指向的底層陣列容量不足時,將會重新分配一個新陣列來儲存資料。而其他 slice 還指向原來的舊底層陣列。
// 超過容量將重新分配陣列來拷貝值、重新儲存 func main() { s1 := []int{1, 2, 3} fmt.Println(len(s1), cap(s1), s1)// 3 3 [1 2 3 ] s2 := s1[1:] fmt.Println(len(s2), cap(s2), s2)// 2 2 [2 3] for i := range s2 { s2[i] += 20 } // 此時的 s1 與 s2 是指向同一個底層陣列的 fmt.Println(s1)// [1 22 23] fmt.Println(s2)// [22 23] s2 = append(s2, 4)// 向容量為 2 的 s2 中再追加元素,此時將分配新陣列來存 for i := range s2 { s2[i] += 10 } fmt.Println(s1)// [1 22 23]// 此時的 s1 不再更新,為舊資料 fmt.Println(s2)// [32 33 14] }
44. 型別宣告與方法
從一個現有的非 interface 型別建立新型別時,並不會繼承原有的方法:
// 定義 Mutex 的自定義型別 type myMutex sync.Mutex func main() { var mtx myMutex mtx.Lock() mtx.UnLock() }
mtx.Lock undefined (type myMutex has no field or method Lock)…
如果你需要使用原型別的方法,可將原型別以匿名欄位的形式嵌到你定義的新 struct 中:
// 型別以欄位形式直接嵌入 type myLocker struct { sync.Mutex } func main() { var locker myLocker locker.Lock() locker.Unlock() }
interface 型別宣告也保留它的方法集:
type myLocker sync.Locker func main() { var locker myLocker locker.Lock() locker.Unlock() }
45. 跳出 for-switch 和 for-select 程式碼塊
沒有指定標籤的 break 只會跳出 switch/select 語句,若不能使用 return 語句跳出的話,可為 break 跳出標籤指定的程式碼塊:
// break 配合 label 跳出指定程式碼塊 func main() { loop: for { switch { case true: fmt.Println("breaking out...") //break// 死迴圈,一直列印 breaking out... break loop } } fmt.Println("out...") }
goto
雖然也能跳轉到指定位置,但依舊會再次進入 for-switch,死迴圈。
46. for 語句中的迭代變數與閉包函式
for 語句中的迭代變數在每次迭代中都會重用,即 for 中建立的閉包函式接收到的引數始終是同一個變數,在 goroutine 開始執行時都會得到同一個迭代值:
func main() { data := []string{"one", "two", "three"} for _, v := range data { go func() { fmt.Println(v) }() } time.Sleep(3 * time.Second) // 輸出 three three three }
最簡單的解決方法:無需修改 goroutine 函式,在 for 內部使用區域性變數儲存迭代值,再傳參:
func main() { data := []string{"one", "two", "three"} for _, v := range data { vCopy := v go func() { fmt.Println(vCopy) }() } time.Sleep(3 * time.Second) // 輸出 one two three }
另一個解決方法:直接將當前的迭代值以引數形式傳遞給匿名函式:
func main() { data := []string{"one", "two", "three"} for _, v := range data { go func(in string) { fmt.Println(in) }(v) } time.Sleep(3 * time.Second) // 輸出 one two three }
注意下邊這個稍複雜的 3 個示例區別:
type field struct { name string } func (p *field) print() { fmt.Println(p.name) } // 錯誤示例 func main() { data := []field{{"one"}, {"two"}, {"three"}} for _, v := range data { go v.print() } time.Sleep(3 * time.Second) // 輸出 three three three } // 正確示例 func main() { data := []field{{"one"}, {"two"}, {"three"}} for _, v := range data { v := v go v.print() } time.Sleep(3 * time.Second) // 輸出 one two three } // 正確示例 func main() { data := []*field{{"one"}, {"two"}, {"three"}} for _, v := range data {// 此時迭代值 v 是三個元素值的地址,每次 v 指向的值不同 go v.print() } time.Sleep(3 * time.Second) // 輸出 one two three }
47. defer 函式的引數值
對 defer 延遲執行的函式,它的引數會在宣告時候就會求出具體值,而不是在執行時才求值:
// 在 defer 函式中引數會提前求值 func main() { var i = 1 defer fmt.Println("result: ", func() int { return i * 2 }()) i++ }
result: 2
48. defer 函式的執行時機
對 defer 延遲執行的函式,會在呼叫它的函式結束時執行,而不是在呼叫它的語句塊結束時執行,注意區分開。
比如在一個長時間執行的函式裡,內部 for 迴圈中使用 defer 來清理每次迭代產生的資源呼叫,就會出現問題:
// 命令列引數指定目錄名 // 遍歷讀取目錄下的檔案 func main() { if len(os.Args) != 2 { os.Exit(1) } dir := os.Args[1] start, err := os.Stat(dir) if err != nil || !start.IsDir() { os.Exit(2) } var targets []string filepath.Walk(dir, func(fPath string, fInfo os.FileInfo, err error) error { if err != nil { return err } if !fInfo.Mode().IsRegular() { return nil } targets = append(targets, fPath) return nil }) for _, target := range targets { f, err := os.Open(target) if err != nil { fmt.Println("bad target:", target, "error:", err)//error:too many open files break } defer f.Close()// 在每次 for 語句塊結束時,不會關閉檔案資源 // 使用 f 資源 } }
先建立 10000 個檔案:
#!/bin/bash for n in {1..10000}; do echo content > "file${n}.txt" done
執行效果:
解決辦法:defer 延遲執行的函式寫入匿名函式中:
// 目錄遍歷正常 func main() { // ... for _, target := range targets { func() { f, err := os.Open(target) if err != nil { fmt.Println("bad target:", target, "error:", err) return// 在匿名函式內使用 return 代替 break 即可 } defer f.Close()// 匿名函式執行結束,呼叫關閉檔案資源 // 使用 f 資源 }() } }
當然你也可以去掉 defer,在檔案資源使用完畢後,直接呼叫f.Close()
來關閉。
49. 失敗的型別斷言
在型別斷言語句中,斷言失敗則會返回目標型別的“零值”,斷言變數與原來變數混用可能出現異常情況:
// 錯誤示例 func main() { var data interface{} = "great" // data 混用 if data, ok := data.(int); ok { fmt.Println("[is an int], data: ", data) } else { fmt.Println("[not an int], data: ", data)// [isn't a int], data:0 } } // 正確示例 func main() { var data interface{} = "great" if res, ok := data.(int); ok { fmt.Println("[is an int], data: ", res) } else { fmt.Println("[not an int], data: ", data)// [not an int], data:great } }
50. 阻塞的 gorutinue 與資源洩露
在 2012 年 Google I/O 大會上,Rob Pike 的Go Concurrency Patterns 演講討論 Go 的幾種基本併發模式,如完整程式碼 中從資料集中獲取第一條資料的函式:
func First(query string, replicas []Search) Result { c := make(chan Result) replicaSearch := func(i int) { c <- replicas[i](query) } for i := range replicas { go replicaSearch(i) } return <-c }
在搜尋重複時依舊每次都起一個 goroutine 去處理,每個 goroutine 都把它的搜尋結果傳送到結果 channel 中,channel 中收到的第一條資料會直接返回。
返回完第一條資料後,其他 goroutine 的搜尋結果怎麼處理?他們自己的協程如何處理?
在First()
中的結果 channel 是無緩衝的,這意味著只有第一個 goroutine 能返回,由於沒有 receiver,其他的 goroutine 會在傳送上一直阻塞。如果你大量呼叫,則可能造成資源洩露。
為避免洩露,你應該確保所有的 goroutine 都能正確退出,有 2 個解決方法:
- 使用帶緩衝的 channel,確保能接收全部 goroutine 的返回結果:
func First(query string, replicas ...Search) Result { c := make(chan Result,len(replicas)) searchReplica := func(i int) { c <- replicas[i](query) } for i := range replicas { go searchReplica(i) } return <-c }
-
使用
select
語句,配合能儲存一個緩衝值的 channeldefault
語句:default
的緩衝 channel 保證了即使結果 channel 收不到資料,也不會阻塞 goroutine
func First(query string, replicas ...Search) Result { c := make(chan Result,1) searchReplica := func(i int) { select { case c <- replicas[i](query): default: } } for i := range replicas { go searchReplica(i) } return <-c }
- 使用特殊的廢棄(cancellation) channel 來中斷剩餘 goroutine 的執行:
func First(query string, replicas ...Search) Result { c := make(chan Result) done := make(chan struct{}) defer close(done) searchReplica := func(i int) { select { case c <- replicas[i](query): case <- done: } } for i := range replicas { go searchReplica(i) } return <-c }
Rob Pike 為了簡化演示,沒有提及演講程式碼中存在的這些問題。不過對於新手來說,可能會不加思考直接使用。
高階篇:51-57
51. 使用指標作為方法的 receiver
只要值是可定址的,就可以在值上直接呼叫指標方法。即是對一個方法,它的 receiver 是指標就足矣。
但不是所有值都是可定址的,比如 map 型別的元素、通過 interface 引用的變數:
type data struct { name string } type printer interface { print() } func (p *data) print() { fmt.Println("name: ", p.name) } func main() { d1 := data{"one"} d1.print()// d1 變數可定址,可直接呼叫指標 receiver 的方法 var in printer = data{"two"} in.print()// 型別不匹配 m := map[string]data{ "x": data{"three"}, } m["x"].print()// m["x"] 是不可定址的// 變動頻繁 }
cannot use data literal (type data) as type printer in assignment: data does not implement printer (print method has pointer receiver) cannot call pointer method on m[“x”] cannot take the address of m[“x”]
52. 更新 map 欄位的值
如果 map 一個欄位的值是 struct 型別,則無法直接更新該 struct 的單個欄位:
// 無法直接更新 struct 的欄位值 type data struct { name string } func main() { m := map[string]data{ "x": {"Tom"}, } m["x"].name = "Jerry" }
cannot assign to struct field m[“x”].name in map
因為 map 中的元素是不可定址的。需區分開的是,slice 的元素可定址:
type data struct { name string } func main() { s := []data{{"Tom"}} s[0].name = "Jerry" fmt.Println(s)// [{Jerry}] }
注意:不久前 gccgo 編譯器可更新 map struct 元素的欄位值,不過很快便修復了,官方認為是 Go1.3 的潛在特性,無需及時實現,依舊在 todo list 中。
更新 map 中 struct 元素的欄位值,有 2 個方法:
- 使用區域性變數
// 提取整個 struct 到區域性變數中,修改欄位值後再整個賦值 type data struct { name string } func main() { m := map[string]data{ "x": {"Tom"}, } r := m["x"] r.name = "Jerry" m["x"] = r fmt.Println(m)// map[x:{Jerry}] }
- 使用指向元素的 map 指標
func main() { m := map[string]*data{ "x": {"Tom"}, } m["x"].name = "Jerry"// 直接修改 m["x"] 中的欄位 fmt.Println(m["x"])// &{Jerry} }
但是要注意下邊這種誤用:
func main() { m := map[string]*data{ "x": {"Tom"}, } m["z"].name = "what???" fmt.Println(m["x"]) }
panic: runtime error: invalid memory address or nil pointer dereference
53. nil interface 和 nil interface 值
雖然 interface 看起來像指標型別,但它不是。interface 型別的變數只有在型別和值均為 nil 時才為 nil
如果你的 interface 變數的值是跟隨其他變數變化的(霧),與 nil 比較相等時小心:
func main() { var data *byte var in interface{} fmt.Println(data, data == nil)// <nil> true fmt.Println(in, in == nil)// <nil> true in = data fmt.Println(in, in == nil)// <nil> false// data 值為 nil,但 in 值不為 nil }
如果你的函式返回值型別是 interface,更要小心這個坑:
// 錯誤示例 func main() { doIt := func(arg int) interface{} { var result *struct{} = nil if arg > 0 { result = &struct{}{} } return result } if res := doIt(-1); res != nil { fmt.Println("Good result: ", res)// Good result:<nil> fmt.Printf("%T\n", res)// *struct {}// res 不是 nil,它的值為 nil fmt.Printf("%v\n", res)// <nil> } } // 正確示例 func main() { doIt := func(arg int) interface{} { var result *struct{} = nil if arg > 0 { result = &struct{}{} } else { return nil// 明確指明返回 nil } return result } if res := doIt(-1); res != nil { fmt.Println("Good result: ", res) } else { fmt.Println("Bad result: ", res)// Bad result:<nil> } }
54. 堆疊變數
你並不總是清楚你的變數是分配到了堆還是棧。
在 C++ 中使用new
建立的變數總是分配到堆記憶體上的,但在 Go 中即使使用new()
、make()
來建立變數,變數為記憶體分配位置依舊歸 Go 編譯器管。
Go 編譯器會根據變數的大小及其 “escape analysis” 的結果來決定變數的儲存位置,故能準確返回本地變數的地址,這在 C/C++ 中是不行的。
在 go build 或 go run 時,加入 -m 引數,能準確分析程式的變數分配位置:
55. GOMAXPROCS、Concurrency(併發)and Parallelism(並行)
Go 1.4 及以下版本,程式只會使用 1 個執行上下文 / OS 執行緒,即任何時間都最多隻有 1 個 goroutine 在執行。
Go 1.5 版本將可執行上下文的數量設定為runtime.NumCPU()
返回的邏輯 CPU 核心數,這個數與系統實際總的 CPU 邏輯核心數是否一致,取決於你的 CPU 分配給程式的核心數,可以使用GOMAXPROCS
環境變數或者動態的使用runtime.GOMAXPROCS()
來調整。
誤區:GOMAXPROCS
表示執行 goroutine 的 CPU 核心數,參考文件
GOMAXPROCS
的值是可以超過 CPU 的實際數量的,在 1.5 中最大為 256
func main() { fmt.Println(runtime.GOMAXPROCS(-1))// 4 fmt.Println(runtime.NumCPU())// 4 runtime.GOMAXPROCS(20) fmt.Println(runtime.GOMAXPROCS(-1))// 20 runtime.GOMAXPROCS(300) fmt.Println(runtime.GOMAXPROCS(-1))// Go 1.9.2 // 300 }
56. 讀寫操作的重新排序
Go 可能會重排一些操作的執行順序,可以保證在一個 goroutine 中操作是順序執行的,但不保證多 goroutine 的執行順序:
var _ = runtime.GOMAXPROCS(3) var a, b int func u1() { a = 1 b = 2 } func u2() { a = 3 b = 4 } func p() { println(a) println(b) } func main() { go u1()// 多個 goroutine 的執行順序不定 go u2() go p() time.Sleep(1 * time.Second) }
執行效果:
如果你想保持多 goroutine 像程式碼中的那樣順序執行,可以使用 channel 或 sync 包中的鎖機制等。
57. 優先排程
你的程式可能出現一個 goroutine 在執行時阻止了其他 goroutine 的執行,比如程式中有一個不讓排程器執行的for
迴圈:
func main() { done := false go func() { done = true }() for !done { } println("done !") }
for
的迴圈體不必為空,但如果程式碼不會觸發排程器執行,將出現問題。
排程器會在 GC、Go 宣告、阻塞 channel、阻塞系統呼叫和鎖操作後再執行,也會在非行內函數呼叫時執行:
func main() { done := false go func() { done = true }() for !done { println("not done !")// 並不內聯執行 } println("done !") }
可以新增-m
引數來分析for
程式碼塊中呼叫的行內函數:
你也可以使用 runtime 包中的Gosched()
來 手動啟動排程器:
func main() { done := false go func() { done = true }() for !done { runtime.Gosched() } println("done !") }
執行效果: