天天看點

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

  

  這種最“老土”的周遊形式應該是所有的語言都通用的吧。

Go語言入門——數組、切片和映射(下)

形式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表示切片的容量,後面在擴容部分會在提到。

Go語言入門——數組、切片和映射(下)

映射周遊

  相較于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])
	}
}
      

  

Go語言入門——數組、切片和映射(下)

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函數之後要指派給對應的切片,因為擴容後和擴容前的記憶體位址變了,如果不做指派,可能會出現使用原來的變量無法通路到新切片的情況。

Go語言入門——數組、切片和映射(下)

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的微信公衆号,我會将我的文章推送給您,并和您一起分享我日常閱讀過的優質文章。