Golang 須要避免踩的 50 個坑1

最近準備寫一些關於golang的技術博文,本文是以前在GitHub上看到的golang技術譯文,感受頗有幫助,先給各位讀者分享一下。php

前言

Go 是一門簡單有趣的編程語言,與其餘語言同樣,在使用時難免會遇到不少坑,不過它們大多不是 Go 自己的設計缺陷。若是你剛從其餘語言轉到 Go,那這篇文章裏的坑多半會踩到。html

若是花時間學習官方 doc、wiki、討論郵件列表、 Rob Pike 的大量文章以及 Go 的源碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減小大量調試代碼的時間。git

初級篇:1-35

1. 左大括號 { 通常不能單獨放一行

在其餘大多數語言中,{ 的位置你自行決定。Go 比較特別,遵照分號注入規則(automatic semicolon injection):編譯器會在每行代碼尾部特定分隔符後加 ; 來分隔多條語句,好比會在 ) 後加分號:github

// 錯誤示例
func main()                    
{
    println("hello world")
}

// 等效於
func main();    // 無函數體                    
{
    println("hello world")
}
./main.go: missing function body
./main.go: syntax error: unexpected semicolon or newline before {
// 正確示例
func main() {
	println("hello world")
}
注意代碼塊等特殊狀況:

// { 並不遵照分號注入規則,不會在其後邊自動加分,此時可換行
func main() {
	{
		println("hello world")
	}
}

  

考:Golang中自動加分號的特殊分隔符golang

2. 未使用的變量

若是在函數體代碼中有未使用的變量,則沒法經過編譯,不過全局變量聲明但不使用是能夠的。編程

即便變量聲明後爲變量賦值,依舊沒法經過編譯,需在某處使用它數組

// 錯誤示例
var gvar int     // 全局變量,聲明不使用也能夠

func main() {
    var one int     // error: one declared and not used
    two := 2    // error: two declared and not used
    var three int    // error: three declared and not used
    three = 3        
}


// 正確示例
// 能夠直接註釋或移除未使用的變量
func main() {
    var one int
    _ = one
    
    two := 2
    println(two)
    
    var three int
    one = three

    var four int
    four = four
}

3. 未使用的 import

若是你 import 一個包,但包中的變量、函數、接口和結構體一個都沒有用到的話,將編譯失敗。app

可使用 _ 下劃線符號做爲別名來忽略導入的包,從而避免編譯錯誤,這隻會執行 package 的 init()編程語言

// 錯誤示例
import (
	"fmt"	// imported and not used: "fmt"
	"log"	// imported and not used: "log"
	"time"	// imported and not used: "time"
)

func main() {
}


// 正確示例
// 可使用 goimports 工具來註釋或移除未使用到的包
import (
	_ "fmt"
	"log"
	"time"
)

func main() {
	_ = log.Println
	_ = time.Now
}

  

4. 簡短聲明的變量只能在函數內部使用

// 錯誤示例
myvar := 1    // syntax error: non-declaration statement outside function body
func main() {
}

// 正確示例
var  myvar = 1
func main() {
}

5. 使用簡短聲明來重複聲明變量

不能用簡短聲明方式來單獨爲一個變量重複聲明, := 左側至少有一個新變量,才容許多變量的重複聲明:ide

// 錯誤示例
func main() {  
    one := 0
    one := 1 // error: no new variables on left side of :=
}

// 正確示例
func main() {
    one := 0
    one, two := 1, 2    // two 是新變量,容許 one 的重複聲明。好比 error 處理常常用同名變量 err
    one, two = two, one    // 交換兩個變量值的簡寫
}

6. 不能使用簡短聲明來設置字段的值

struct 的變量字段不能使用 := 來賦值以使用預約義的變量來避免解決:

// 錯誤示例
type info struct {
    result int
}

func work() (int, error) {
    return 3, nil
}

func main() {
    var data info
    data.result, err := work()    // error: non-name data.result on left side of :=
    fmt.Printf("info: %+v\n", data)
}

// 正確示例
func main() {
    var data info
    var err error    // err 須要預聲明

    data.result, err = work()
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("info: %+v\n", data)
}

7. 不當心覆蓋了變量

對從動態語言轉過來的開發者來講,簡短聲明很好用,這可能會讓人誤會 := 是一個賦值操做符。

若是你在新的代碼塊中像下邊這樣誤用了 :=,編譯不會報錯,可是變量不會按你的預期工做:

func main() {
    x := 1
    println(x)        // 1
    {
        println(x)    // 1
        x := 2
        println(x)    // 2    // 新的 x 變量的做用域只在代碼塊內部
    }
    println(x)        // 1
}

這是 Go 開發者常犯的錯,並且不易被發現。

可以使用 vet 工具來診斷這種變量覆蓋,Go 默認不作覆蓋檢查,添加 -shadow 選項來啓用:

> go tool vet -shadow main.go
main.go:9: declaration of "x" shadows declaration at main.go:5
注意 vet 不會報告所有被覆蓋的變量,可使用 go-nyet 來作進一步的檢測:

> $GOPATH/bin/go-nyet main.go
main.go:10:3:Shadowing variable `x`

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)
}

10. map 容量

在建立 map 類型的變量時能夠指定容量,但不能像 slice 同樣使用 cap() 來檢測分配空間的大小:

// 錯誤示例
func main() {
    m := make(map[string]int, 99)
    println(cap(m))     // error: invalid argument m1 (type map[string]int) for cap  
}

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"
    }
}

12. Array 類型的值做爲函數參數

在 C/C++ 中,數組(名)是指針。將數組做爲參數傳進函數時,至關於傳遞了數組內存地址的引用,在函數內部會改變該數組的值。

在 Go 中,數組是值。做爲參數傳進函數時,傳遞的是數組的原始值拷貝,此時在函數內部是沒法更新該數組的:

// 數組使用值拷貝傳參
func main() {
    x := [3]int{1,2,3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr)    // [7 2 3]
    }(x)
    fmt.Println(x)            // [1 2 3]    // 並非你覺得的 [7 2 3]
}

若是想修改參數數組:

  • 直接傳遞指向這個數組的指針類型:
  • // 傳址會修改原數據
    func main() {
        x := [3]int{1,2,3}
    
        func(arr *[3]int) {
            (*arr)[0] = 7    
            fmt.Println(arr)    // &[7 2 3]
        }(&x)
        fmt.Println(x)    // [7 2 3]
    }

    直接使用 slice:即便函數內部獲得的是 slice 的值拷貝,但依舊會更新 slice 的原始數據(底層 array)

  • // 會修改 slice 的底層 array,從而修改 slice
    func main() {
        x := []int{1, 2, 3}
        func(arr []int) {
            arr[0] = 7
            fmt.Println(x)    // [7 2 3]
        }(x)
        fmt.Println(x)    // [7 2 3]
    }

    13. range 遍歷 slice 和 array 時混淆了返回值

    與其餘編程語言中的 for-in 、foreach 遍歷語句不一樣,Go 中的 range 在遍歷時會生成 2 個值,第一個是元素索引,第二個是元素的值:

  • // 錯誤示例
    func main() {
        x := []string{"a", "b", "c"}
        for v := range x {
            fmt.Println(v)    // 1 2 3
        }
    }
    
    
    // 正確示例
    func main() {
        x := []string{"a", "b", "c"}
        for _, v := range x {    // 使用 _ 丟棄索引
            fmt.Println(v)
        }
    }

    14. slice 和 array 實際上是一維數據

    看起來 Go 支持多維的 array 和 slice,能夠建立數組的數組、切片的切片,但其實並非。

    對依賴動態計算多維數組值的應用來講,就性能和複雜度而言,用 Go 實現的效果並不理想。

    可使用原始的一維數組、「獨立「 的切片、「共享底層數組」的切片來建立動態的多維數組。

    1. 使用原始的一維數組:要作好索引檢查、溢出檢測、以及當數組滿時再添加值時要從新作內存分配。

    2. 使用「獨立」的切片分兩步:

    • 建立外部 slice
    • 對每一個內部 slice 進行內存分配

      注意內部的 slice 相互獨立,使得任一內部 slice 增縮都不會影響到其餘的 slice

    • // 使用各自獨立的 6 個 slice 來建立 [2][3] 的動態多維數組
      func main() {
          x := 2
          y := 4
          
          table := make([][]int, x)
          for i  := range table {
              table[i] = make([]int, y)
          }
      }
      1. 使用「共享底層數組」的切片
      • 建立一個存放原始數據的容器 slice
      • 建立其餘的 slice
      • 切割原始 slice 來初始化其餘的 slice
      • func main() {
            h, w := 2, 4
            raw := make([]int, h*w)
        
            for i := range raw {
                raw[i] = i
            }
        
            // 初始化原始 slice
            fmt.Println(raw, &raw[4])    // [0 1 2 3 4 5 6 7] 0xc420012120 
            
            table := make([][]int, h)
            for i := range table {
                
                // 等間距切割原始 slice,建立動態多維數組 table
                // 0: raw[0*4: 0*4 + 4]
                // 1: raw[1*4: 1*4 + 4]
                table[i] = raw[i*w : i*w + w]
            }
        
            fmt.Println(table, &table[1][0])    // [[0 1 2 3] [4 5 6 7]] 0xc420012120
        }

        更多關於多維數組的參考

        go-how-is-two-dimensional-arrays-memory-representation

        what-is-a-concise-way-to-create-a-2d-slice-in-go

        15. 訪問 map 中不存在的 key

        和其餘編程語言相似,若是訪問了 map 中不存在的 key 則但願能返回 nil,好比在 PHP 中:

      • > php -r '$v = ["x"=>1, "y"=>2]; @var_dump($v["z"]);'
        NULL

        Go 則會返回元素對應數據類型的零值,好比 nil'' 、false 和 0,取值操做總有值返回,故不能經過取出來的值來判斷 key 是否是在 map 中。

        檢查 key 是否存在能夠用 map 直接訪問,檢查返回的第二個參數便可:

      • // 錯誤的 key 檢測方式
        func main() {
            x := map[string]string{"one": "2", "two": "", "three": "3"}
            if v := x["two"]; v == "" {
                fmt.Println("key two is no entry")    // 鍵 two 存不存在都會返回的空字符串
            }
        }
        
        // 正確示例
        func main() {
            x := map[string]string{"one": "2", "two": "", "three": "3"}
            if _, ok := x["two"]; !ok {
                fmt.Println("key two is no entry")
            }
        }

        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
        }

        17. string 與 byte slice 之間的轉換

        當進行 string 和 byte slice 相互轉換時,參與轉換的是拷貝的原始值。這種轉換的過程,與其餘編程語的強制類型轉換操做不一樣,也和新 slice 與舊 slice 共享底層數組不一樣。

        Go 在 string 與 byte slice 相互轉換上優化了兩點,避免了額外的內存分配:

        • 在 map[string] 中查找 key 時,使用了對應的 []byte,避免作 m[string(key)] 的內存分配
        • 使用 for range 迭代 string 轉換爲 []byte 的迭代:for i,v := range []byte(str) {...}

        霧:參考原文

相關文章
相關標籤/搜索