Golang 新手可能會踩的 N 個坑

原文: 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
}

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章