Go語言入門——陣列、切片和對映(下)
上篇主要介紹了Go語言裡面常見的複合資料型別的宣告和初始化。
這篇主要針對陣列、切片和對映這些複合資料型別從其他幾個方面介紹比較下。
1、遍歷
不管是陣列、切片還是對映結構,都是一種集合型別,要從這些集合取出元素就要查詢或者遍歷。
對於從其他語言轉到Go語言,在遍歷這邊還是有稍稍不同的。
陣列遍歷
形式1
package main import "fmt" func main() { arr := [5]int{1, 2, 3, 4, 5} for i := 0; i < len(arr); i++ { fmt.Println(arr[i]) } }
這種最“老土”的遍歷形式應該是所有的語言都通用的吧。
形式2
package main import "fmt" func main() { arr := [5]int{1, 2, 3, 4, 5} for index, value := range arr { fmt.Println(index, value) } }
range關鍵字表示遍歷,後面在切片和對映的遍歷我們也可以看到。
這個遍歷就有點Java裡面的增強for的味道了。
但是還有有點不一樣,我前兩天剛寫Go程式碼的時候還在這裡掉坑裡了。
for關鍵字後面有兩個變數,一個是index即陣列角標表示第幾個元素,一個是value即每個元素的值。
坑就坑在,如果只有一個變數也是可以遍歷的,比如這樣
func main() { arr := [5]int{1, 2, 3, 4, 5} for v := range arr { fmt.Println(v) } }
這樣和Java的增強for迴圈遍歷幾乎就一樣了,所以我想當然的以為這裡的v就是arr對應的每個元素值。
但其實不是,這裡v表示的是陣列角標。所以如果按照這樣的寫法本以為取到的是陣列的值,其實是陣列的角標值。
另外,Go語言中有一個特性,對於有些用不上的變數,可以使用"_"代替,比如上面的程式碼可以寫成
func main() { arr := [5]int{1, 2, 3, 4, 5} for _, value := range arr { fmt.Println(value) } }
切片遍歷
切片的遍歷和陣列沒有什麼區別。
package main import "fmt" func main() { s := []int{1, 2, 3, 4, 5} for i := 0; i < len(s); i++ { fmt.Println(s[i]) } for index, v := range s { fmt.Println(index, v) } }
兩種遍歷方式也都是適用的。
注意這裡len函式表示獲取切片的長度,除此以外,切片還有一個數組沒有的函式即cap,cap表示切片的容量,後面在擴容部分會在提到。
對映遍歷
相較於Java裡面對於Map遍歷與其他集合遍歷有些差別來說,Go裡面對於Map的遍歷與其他集合的遍歷倒顯得比較一致。
package main import "fmt" func main() { m := make(map[string]string) m["Jackie"] = "Zheng" m["Location"] = "Shanghai" for key, value := range m { fmt.Println(key, value) } }
除此以外,我們可以只針對key進行遍歷,如下
func main() { m := make(map[string]string) m["Jackie"] = "Zheng" m["Location"] = "Shanghai" for key := range m { fmt.Println(key, m[key]) } }
2、切片擴容
陣列和struct結構體都是靜態資料,陣列是定長的,而切片和對映都是動態資料型別。
為什麼說是動態資料型別?
上面有順帶提過,切片除了有長度len的概念,還有容量的概念。上篇說到切片宣告初始化的一種方式
s := make([]int, 3, 5) // 3所在位置表示切片長度,5所在位置表示容量即最大可能儲存的元素個數
我們可以動態向切片新增或者刪除元素。
如果新新增元素後已經超出切片原來的容量,那麼就會擴容了。借用Go聖經裡面的例子
var x, y []int for i := 0; i < 10; i++ { y = append(x, i) fmt.Printf("%d cap=%d\t %v\n", i, cap(y), y) x = y }
使用append新增新元素每次都會校驗當前切片的長度如果已經達到最大容量,則會考慮先擴容,從執行結果可以看出每次擴容是原來的兩倍,實際的擴容過程是會先建立一個兩倍長的底層陣列,然後將原切片資料拷貝到這個底層陣列,再新增要插入的元素。
所以,這裡append函式之後要賦值給對應的切片,因為擴容後和擴容前的記憶體地址變了,如果不做賦值,可能會出現使用原來的變數無法訪問到新切片的情況。
3、傳值還是傳引用
首先來看一個數組的例子
package main import "fmt" func main() { var arr = [5]int{1, 2, 3, 4, 5} fmt.Println(arr) fmt.Printf("origin array address: %p \n", &arr) passArray(arr) fmt.Println(arr) } func passArray (arr1 [5]int) { fmt.Printf("passed array address, arr1: %p \n", &arr1) fmt.Println(arr1) arr1[3] = 111 fmt.Println("pass array arr1: ", arr1) }
執行結果如下
[1 2 3 4 5] origin array address: 0xc000090000 passed array address, arr1: 0xc000090060 [1 2 3 4 5] pass array arr1: [1 2 3 111 5] [1 2 3 4 5]
- 先列印該陣列,沒有問題
- 在列印當前陣列的地址為:0xc000090000
- 再呼叫函式passArray,先列印改陣列地址為:0xc000090060,可以看出這裡的地址和原始陣列的地址不一樣,這是因為這裡傳的是一個數組的副本,並非指向原陣列
- 然後列印arr1陣列,和原陣列資料一致
- 再更新角標為3的元素值為111,列印後的結果為:[1 2 3 111 5]。可以發現arr1陣列已經更新了
- 呼叫完成passArray後,在列印原始陣列,發現數據仍為:[1 2 3 4 5]並沒有因為arr1的更新而受影響。
這是因為,在呼叫函式passArray時,傳的是arr陣列的一個副本,重新開闢了一個新空間儲存這5個數組元素,不同記憶體空間的陣列變動是不會影響另一塊儲存陣列元素的記憶體空間的。
這種陣列傳遞是非常笨重的,因為需要重新開闢一塊空間把原來的陣列copy一份,這裡是5個元素,如果是1000或者10000個元素呢?所以,我們可以通過其他的方式規避這種笨重的操作,沒錯,就是指標,程式碼如下
package main import "fmt" func main() { var arr = [5]int{1, 2, 3, 4, 5} fmt.Println(arr) fmt.Printf("origin array address: %p \n", &arr) passAddress(&arr) fmt.Println(arr) } func passAddress (arr2 *[5]int) { fmt.Printf("passed array address, arr2: %p \n", arr2) fmt.Printf("passed array address, arr2: %p \n", &arr2) fmt.Println(arr2) arr2[3] = 111 fmt.Println("pass array arr2: ", *arr2) }
執行結果如下
[1 2 3 4 5] origin array address: 0xc000084000 passed array address, arr2: 0xc000084000 passed array address, arr2: 0xc00000e010 &[1 2 3 4 5] pass array arr2: [1 2 3 111 5] [1 2 3 111 5]
- 先列印該陣列,沒有問題
- 在列印當前陣列的地址為:0xc000084000
- 然後呼叫函式passAddress,注意這裡傳的是陣列的地址,接收的是一個指標型別變數arr2。第一次我們直接列印arr2,得到地址為:0xc000084000。沒錯,這裡的意思是arr2這個指標指向的記憶體地址就是0xc000084000,即和原始陣列指向的是同一塊記憶體區域,也就是指向同一塊儲存這5個元素的區域。
- 緊接著,列印arr2的地址,這個&arr2的意思是arr2這個指標的地址,為0xc00000e010,通過上面一點,我們已經知道這個指標指向的地址是0xc000084000
- 然後我們列印arr2,得到&[1 2 3 4 5]
- 之後我們再改變第三個角標的值為111,並列印arr2指標指向的陣列的值為:[1 2 3 111 5],即arr2中元素已經更新
- 呼叫完passAddress後,我們再次列印原始陣列,得到的是:[1 2 3 111 5]
原始陣列的值被改變了,這是因為我們傳遞的是一個引用,通過一個地址指向了原來陣列儲存的地址。所以在函式passAddress中實際上是對原來的記憶體空間的資料更新,顯然也會反應到原來的陣列上。
如上是陣列傳值的例子,slice和map也是傳值的。雖然我們在傳遞slice或者map的時候沒有顯式使用指標,但是他們的內部結構都間接使用了指標,所以slice和map都是引用型別,傳遞的時候相當於傳遞的是指標的副本,可以理解為上面陣列中傳指標的例子。
如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。