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>