原文: https://segmentfault.com/a/1190000013739000#articleHeader9
8. 顯式類型的變量無法使用 nil 來初始化
nil
是 interface、function、pointer、map、slice 和 channel 類型變量的默認初始值。但聲明時不指定類型,編譯器也無法推斷出變量的具體類型。
// 錯誤示例
func main() {
var x = nil // error: use of untyped nil
_ = x
}
// 正確示例
func main() {
var x interface{} = nil
_ = x
}
9. 直接使用值爲 nil 的 slice、map
允許對值爲 nil 的 slice 添加元素,但對值爲 nil 的 map 添加元素則會造成運行時 panic
// map 錯誤示例
func main() {
var m map[string]int
m["one"] = 1 // error: panic: assignment to entry in nil map
// m := make(map[string]int)// map 的正確聲明,分配了實際的內存
}
// slice 正確示例
func main() {
var s []int
s = append(s, 1)
}
11. string 類型的變量值不能爲 nil
對那些喜歡用 nil
初始化字符串的人來說,這就是坑:
// 錯誤示例
func main() {
var s string = nil // cannot use nil as type string in assignment
if s == nil { // invalid operation: s == nil (mismatched types string and nil)
s = "default"
}
}
// 正確示例
func main() {
var s string // 字符串類型的零值是空串 ""
if s == "" {
s = "default"
}
}
16. string 類型的值是常量,不可更改
嘗試使用索引遍歷字符串,來更新字符串中的個別字符,是不允許的。
string 類型的值是隻讀的二進制 byte slice,如果真要修改字符串中的字符,將 string 轉爲 []byte 修改後,再轉爲 string 即可:
// 修改字符串的錯誤示例
func main() {
x := "text"
x[0] = "T" // error: cannot assign to x[0]
fmt.Println(x)
}
// 修改示例
func main() {
x := "text"
xBytes := []byte(x)
xBytes[0] = 'T' // 注意此時的 T 是 rune 類型
x = string(xBytes)
fmt.Println(x) // Text
}
注意: 上邊的示例並不是更新字符串的正確姿勢,因爲一個 UTF8 編碼的字符可能會佔多個字節,比如漢字就需要 3~4 個字節來存儲,此時更新其中的一個字節是錯誤的。
更新字串的正確姿勢:將 string 轉爲 rune slice(此時 1 個 rune 可能佔多個 byte),直接更新 rune 中的字符
func main() {
x := "text"
xRunes := []rune(x)
xRunes[0] = '我'
x = string(xRunes)
fmt.Println(x) // 我ext
}
21. 在多行 array、slice、map 語句中缺少 ,
號
func main() {
x := []int {
1,
2 // syntax error: unexpected newline, expecting comma or }
}
y := []int{1,2,}
z := []int{1,2}
// ...
}
22. log.Fatal
和 log.Panic
不只是 log
log 標準庫提供了不同的日誌記錄等級,與其他語言的日誌庫不同,Go 的 log 包在調用 Fatal*()
、Panic*()
時能做更多日誌外的事,如中斷程序的執行等:
func main() {
log.Fatal("Fatal level log: log entry") // 輸出信息後,程序終止執行
log.Println("Nomal level log: log entry")
}
23. 對內建數據結構的操作並不是同步的
儘管 Go 本身有大量的特性來支持併發,但並不保證併發的數據安全,用戶需自己保證變量等數據以原子操作更新。
goroutine 和 channel 是進行原子操作的好方法,或使用 "sync" 包中的鎖。
25. range 迭代 map
如果你希望以特定的順序(如按 key 排序)來迭代 map,要注意每次迭代都可能產生不一樣的結果。
Go 的運行時是有意打亂迭代順序的,所以你得到的迭代結果可能不一致。但也並不總會打亂,得到連續相同的 5 個迭代結果也是可能的,如:
func main() {
m := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}
for k, v := range m {
fmt.Println(k, v)
}
}
如果你去 Go Playground 重複運行上邊的代碼,輸出是不會變的,只有你更新代碼它纔會重新編譯。重新編譯後迭代順序是被打亂的:
27. 自增和自減運算
很多編程語言都自帶前置後置的 ++
、--
運算。但 Go 特立獨行,去掉了前置操作,同時 ++
、—
只作爲運算符而非表達式。
// 錯誤示例
func main() {
data := []int{1, 2, 3}
i := 0
++i // syntax error: unexpected ++, expecting }
fmt.Println(data[i++]) // syntax error: unexpected ++, expecting :
}
// 正確示例
func main() {
data := []int{1, 2, 3}
i := 0
i++
fmt.Println(data[i]) // 2
}
30. 不導出的 struct 字段無法被 encode
以小寫字母開頭的字段成員是無法被外部直接訪問的,所以 struct
在進行 json、xml、gob 等格式的 encode 操作時,這些私有字段會被忽略,導出時得到零值:
func main() {
in := MyData{1, "two"}
fmt.Printf("%#v\n", in) // main.MyData{One:1, two:"two"}
encoded, _ := json.Marshal(in)
fmt.Println(string(encoded)) // {"One":1} // 私有字段 two 被忽略了
var out MyData
json.Unmarshal(encoded, &out)
fmt.Printf("%#v\n", out) // main.MyData{One:1, two:""}
}
33. 向已關閉的 channel 發送數據會造成 panic
從已關閉的 channel 接收數據是安全的:
接收狀態值 ok
是 false
時表明 channel 中已沒有數據可以接收了。類似的,從有緩衝的 channel 中接收數據,緩存的數據獲取完再沒有數據可取時,狀態值也是 false
向已關閉的 channel 中發送數據會造成 panic:
func main() {
ch := make(chan int)
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- idx
}(i)
}
fmt.Println(<-ch) // 輸出第一個發送的值
close(ch) // 不能關閉,還有其他的 sender
time.Sleep(2 * time.Second) // 模擬做其他的操作
}
34. 使用了值爲 nil
的 channel
在一個值爲 nil 的 channel 上發送和接收數據將永久阻塞:
func main() {
var ch chan int // 未初始化,值爲 nil
for i := 0; i < 3; i++ {
go func(i int) {
ch <- i
}(i)
}
fmt.Println("Result: ", <-ch)
time.Sleep(2 * time.Second)
}
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)
}
}
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}
}
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
}
47. defer 函數的參數值
對 defer 延遲執行的函數,它的參數會在聲明時候就會求出具體值,而不是在執行時才求值:
// 在 defer 函數中參數會提前求值
func main() {
var i = 1
defer fmt.Println("result: ", func() int { return i * 2 }())
i++
}
result: 2
48. defer 函數的執行時機
對 defer 延遲執行的函數,會在調用它的函數結束時執行,而不是在調用它的語句塊結束時執行,注意區分開。
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
}