Golang新手可能會踩的10個坑初級篇10-20

10.map 容量

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

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

11.string 類型的變量值不能為 nil

對那些喜歡用 nil 初始化字符串的人來說,這就是坑:

<code>// 錯誤示例
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"
    }
}
/<code>

12.Array 類型的值作為函數參數

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

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

<code>// 數組使用值拷貝傳參
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]
}
/<code>

如果想修改參數數組:

  • 直接傳遞指向這個數組的指針類型:
<code>// 傳址會修改原數據
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]
}
/<code>
  • 直接使用 slice:即使函數內部得到的是 slice 的值拷貝,但依舊會更新 slice 的原始數據(底層 array)
<code>// 會修改 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]
}
/<code>

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

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

<code>// 錯誤示例
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)
    }
}
/<code>

14.slice 和 array 其實是一維數據

看起來 Go 支持多維的 array 和 slice,可以創建數組的數組、切片的切片,但其實並不是。

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

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

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

2.使用“獨立”的切片分兩步:

  • 創建外部 slice對每個內部 slice 進行內存分配注意內部的 slice 相互獨立,使得任一內部 slice 增縮都不會影響到其他的 slice
<code>// 使用各自獨立的 6 個 slice 來創建 [2][3] 的動態多維數組
func main() {
    x := 2
    y := 4

    table := make([][]int, x)
    for i  := range table {
        table[i] = make([]int, y)
    }
}
/<code>

1.使用“共享底層數組”的切片

  • 創建一個存放原始數據的容器 slice
  • 創建其他的 slice
  • 切割原始 slice 來初始化其他的 slice
<code>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
}
/<code>

更多關於多維數組的參考

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 中:

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

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

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

<code>// 錯誤的 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")
    }
}
/<code>

16.string 類型的值是常量,不可更改

嘗試使用索引遍歷字符串,來更新字符串中的個別字符,是不允許的。

string 類型的值是隻讀的二進制 byte slice,如果真要修改字符串中的字符,將 string 轉為 []byte 修改後,再轉為 string 即可:

<code>// 修改字符串的錯誤示例
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
}
/<code>

注意: 上邊的示例並不是更新字符串的正確姿勢,因為一個 UTF8 編碼的字符可能會佔多個字節,比如漢字就需要 3~4個字節來存儲,此時更新其中的一個字節是錯誤的。

更新字串的正確姿勢:將 string 轉為 rune slice(此時 1 個 rune 可能佔多個 byte),直接更新 rune 中的字符

<code>func main() {
    x := "text"
    xRunes := []rune(x)
    xRunes[0] = '我'
    x = string(xRunes)
    fmt.Println(x)    // 我ext
}
/<code>

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

18.string 與索引操作符

對字符串用索引訪問返回的不是字符,而是一個 byte 值。

這種處理方式和其他語言一樣,比如 PHP 中:

<code>> php -r '$name="中文"; var_dump($name);'    # "中文" 佔用 6 個字節
string(6) "中文"

> php -r '$name="中文"; var_dump($name[0]);' # 把第一個字節當做 Unicode 字符讀取,顯示 U+FFFD
string(1) "�"    

> php -r '$name="中文"; var_dump($name[0].$name[1].$name[2]);'
string(3) "中"
/<code>
<code>func main() {
    x := "ascii"
    fmt.Println(x[0])        // 97
    fmt.Printf("%T\n", x[0])// uint8
}
/<code>

如果需要使用 for range 迭代訪問字符串中的字符(unicode code point / rune),標準庫中有 "unicode/utf8" 包來做 UTF8 的相關解碼編碼。另外 utf8string 也有像 func (s *String) At(i int) rune 等很方便的庫函數。

19.字符串並不都是 UTF8 文本

string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值時才是 UTF8 文本,字串可以通過轉義來包含其他數據。

判斷字符串是否是 UTF8 文本,可使用 "unicode/utf8" 包中的 ValidString() 函數:

<code>func main() {
    str1 := "ABC"
    fmt.Println(utf8.ValidString(str1))    // true

    str2 := "A\xfeC"
    fmt.Println(utf8.ValidString(str2))    // false

    str3 := "A\\xfeC"
    fmt.Println(utf8.ValidString(str3))    // true    // 把轉義字符轉義成字面值
}
/<code>

20.字符串的長度

在 Python 中:

<code>    data = u'♥'  
    print(len(data)) # 1
/<code>

然而在 Go 中:

<code>func main() {
    char := "♥"
    fmt.Println(len(char))    // 3
}
/<code>

Go 的內建函數 len() 返回的是字符串的 byte 數量,而不是像 Python 中那樣是計算 Unicode 字符數。

如果要得到字符串的字符數,可使用 "unicode/utf8" 包中的 RuneCountInString(str string) (n int)

<code>func main() {
    char := "♥"
    fmt.Println(utf8.RuneCountInString(char))    // 1
}
/<code>

注意: RuneCountInString 並不總是返回我們看到的字符數,因為有的字符會佔用 2 個 rune:

<code>func main() {
    char := "é"
    fmt.Println(len(char))    // 3
    fmt.Println(utf8.RuneCountInString(char))    // 2
    fmt.Println("cafe\u0301")    // café    // 法文的 cafe,實際上是兩個 rune 的組合
}/<code>


分享到:


相關文章: