Go快速入門 07 | 集合型別: array、slice 和 map的使用

Swenson1992發表於2021-02-04

在實際需求中,會有很多同一型別的元素放在一起的場景,這就是集合,例如 100 個數字,10 個字串等。在 Go 語言中,陣列(array)、切片(slice)、對映(map)這些都是集合型別,用於存放同一類元素。

Array(陣列)

陣列存放的是固定長度、相同型別的資料,而且這些存放的元素是連續的。所存放的資料型別沒有限制,可以是整型、字串甚至自定義。

陣列宣告

在下面的程式碼示例中,宣告瞭一個字串陣列,長度是 5,所以其型別定義為 [5]string,其中大括號中的元素用於初始化陣列。此外,在型別名前加 [] 中括號,並設定好長度,就可以通過它來推測陣列的型別。

注意:[5]string 和 [4]string 不是同一種型別,也就是說長度也是陣列型別的一部分。

array:=[5]string{"a","b","c","d","e"}

陣列在記憶體中都是連續存放的,下面通過一幅圖片形象地展示陣列在記憶體中如何存放:

Go快速入門 07 | 集合型別:如何正確使用 array、slice 和 map

可以看到,陣列的每個元素都是連續存放的,每一個元素都有一個下標(Index)。下標從 0 開始,比如第一個元素 a 對應的下標是 0,第二個元素 b 對應的下標是 1。以此類推,通過 array+[下標] 的方式,可以快速地定位元素。

如下面程式碼所示,執行它,可以看到輸出列印的結果是 c,也就是陣列 array 的第三個元素:

func main() {
    array:=[5]string{"a","b","c","d","e"}
    fmt.Println(array[2])
}

在定義陣列的時候,陣列的長度可以省略,這個時候 Go 語言會自動根據大括號 {} 中元素的個數推匯出長度,所以以上示例也可以像下面這樣宣告:

array:=[...]string{"a","b","c","d","e"}

以上省略陣列長度的宣告只適用於所有元素都被初始化的陣列,如果是隻針對特定索引元素初始化的情況,就不適合了,如下示例:

array1:=[5]string{1:"b",3:"d"}

示例中的「1:”b”,3:”d”」的意思表示初始化索引 1 的值為 b,初始化索引 3 的值為 d,整個陣列的長度為 5。如果省略長度 5,那麼整個陣列的長度只有 4,顯然不符合定義陣列的初衷。

此外,沒有初始化的索引,其預設值都是陣列型別的零值,也就是 string 型別的零值 “” 空字串。

除了使用 [] 操作符根據索引快速定位陣列的元素外,還可以通過 for 迴圈列印所有的陣列元素,如下面的程式碼所示:

for i:=0;i<5;i++{
    fmt.Printf("陣列索引:%d,對應值:%s\n", i, array[i])
}

陣列迴圈

使用傳統的 for 迴圈遍歷陣列,輸出對應的索引和對應的值,這種方式很煩瑣,一般不使用,大部分情況下,使用的是 for range 這種 Go 語言的新型迴圈,如下面的程式碼所示:

for i,v:=range array{
    fmt.Printf("陣列索引:%d,對應值:%s\n", i, v)
}

這種方式和傳統 for 迴圈的結果是一樣的。對於陣列,range 表示式返回兩個結果:

  1. 第一個是陣列的索引;
  2. 第二個是陣列的值。

在上面的示例中,把返回的兩個結果分別賦值給 i 和 v 這兩個變數,就可以使用它們了。

相比傳統的 for 迴圈,for range 要更簡潔,如果返回的值用不到,可以使用 _ 下劃線丟棄,如下面的程式碼所示:

for _,v:=range array{
    fmt.Printf("對應值:%s\n", v)
}

陣列的索引通過 _ 就被丟棄了,只使用陣列的值 v 即可。

Slice(切片)

切片和陣列類似,可以把它理解為動態陣列。切片是基於陣列實現的,它的底層就是一個陣列。對陣列任意分隔,就可以得到一個切片。現在通過一個例子來更好地理解它,同樣還是基於上述例子的 array。

基於陣列生成切片

下面程式碼中的 array[2:5] 就是獲取一個切片的操作,它包含從陣列 array 的索引 2 開始到索引 5 結束的元素:

array:=[5]string{"a","b","c","d","e"}
slice:=array[2:5]
fmt.Println(slice)

注意:這裡是包含索引 2,但是不包含索引 5 的元素,即在 : 右邊的數字不會被包含。

//基於陣列生成切片,包含索引start,但是不包含索引end
slice:=array[start:end]

所以 array[2:5] 獲取到的是 c、d、e 這三個元素,然後這三個元素作為一個切片賦值給變數 slice。

切片和陣列一樣,也可以通過索引定位元素。這裡以新獲取的 slice 切片為例,slice[0] 的值為 c,slice[1] 的值為 d。

有沒有發現,在陣列 array 中,元素 c 的索引其實是 2,但是對陣列切片後,在新生成的切片 slice 中,它的索引是 0,這就是切片。雖然切片底層用的也是 array 陣列,但是經過切片後,切片的索引範圍改變了。

通過下圖可以看出,切片是一個具備三個欄位的資料結構,分別是指向陣列的指標 data,長度 len 和容量 cap:

Go快速入門 07 | 集合型別:如何正確使用 array、slice 和 map

這裡有一些小技巧,切片表示式 array[start:end] 中的 start 和 end 索引都是可以省略的,如果省略 start,那麼 start 的值預設為 0,如果省略 end,那麼 end 的預設值為陣列的長度。如下面的示例:

  • array[:4] 等價於 array[0:4]。
  • array[1:] 等價於 array[1:5]。
  • array[:] 等價於 array[0:5]。

切片修改

切片的值也可以被修改,這裡也同時可以證明切片的底層是陣列。

對切片相應的索引元素賦值就是修改,在下面的程式碼中,把切片 slice 索引 1 的值修改為 f,然後列印輸出陣列 array:

slice:=array[2:5]
slice[1] ="f"
fmt.Println(array)

可以看到如下結果:

[a b c f e]

陣列對應的值已經被修改為 f,所以這也證明了基於陣列的切片,使用的底層陣列還是原來的陣列,一旦修改切片的元素值,那麼底層陣列對應的值也會被修改。

切片宣告

除了可以從一個陣列得到切片外,還可以宣告切片,比較簡單的是使用 make 函式。

下面的程式碼是宣告瞭一個元素型別為 string 的切片,長度是 4,make 函式還可以傳入一個容量引數:

slice1:=make([]string,4)

在下面的例子中,指定了新建立的切片 []string 容量為 8:

slice1:=make([]string,4,8)

這裡需要注意的是,切片的容量不能比切片的長度小。

切片的長度你已經知道了,就是切片內元素的個數。那麼容量是什麼呢?其實就是切片的空間。

Go 語言在記憶體上劃分了一塊容量為 8 的內容空間(容量為 8),但是隻有 4 個記憶體空間才有元素(長度為 4),其他的記憶體空間處於空閒狀態,當通過 append 函式往切片中追加元素的時候,會追加到空閒的記憶體上,當切片的長度要超過容量的時候,會進行擴容。

切片不僅可以通過 make 函式宣告,也可以通過字面量的方式宣告和初始化,如下所示:

slice1:=[]string{"a","b","c","d","e"}
fmt.Println(len(slice1),cap(slice1))

可以注意到,切片和陣列的字面量初始化方式,差別就是中括號 [] 裡的長度。此外,通過字面量初始化的切片,長度和容量相同。

Append

我們可以通過內建的 append 函式對一個切片追加元素,返回新切片,如下面的程式碼所示:

//追加一個元素
slice2:=append(slice1,"f")
//多加多個元素
slice2:=append(slice1,"f","g")
//追加另一個切片
slice2:=append(slice1,slice...)

append 函式可以有以上三種操作,你可以根據自己的實際需求進行選擇,append 會自動處理切片容量不足需要擴容的問題。

小技巧:在建立新切片的時候,最好要讓新切片的長度和容量一樣,這樣在追加操作的時候就會生成新的底層陣列,從而和原有陣列分離,就不會因為共用底層陣列導致修改內容的時候影響多個切片。

切片元素迴圈

切片的迴圈和陣列一模一樣,常用的也是 for range 方式。

在 Go 語言開發中,切片是使用最多的,尤其是作為函式的引數時,相比陣列,通常會優先選擇切片,因為它高效,記憶體佔用小。

Map(對映)

在 Go 語言中,map 是一個無序的 K-V 鍵值對集合,結構為 map[K]V。其中 K 對應 Key,V 對應 Value。map 中所有的 Key 必須具有相同的型別,Value 也同樣,但 Key 和 Value 的型別可以不同。此外,Key 的型別必須支援 == 比較運算子,這樣才可以判斷它是否存在,並保證 Key 的唯一。

Map 宣告初始化

建立一個 map 可以通過內建的 make 函式,如下面的程式碼所示:

nameAgeMap:=make(map[string]int)

它的 Key 型別為 string,Value 型別為 int。有了建立好的 map 變數,就可以對它進行操作了。

在下面的示例中,我新增了一個鍵值對,Key 為Golang,Value 為 5,如果 Key 已經存在,則更新 Key 對應的 Value:

複製程式碼
nameAgeMap[“Golang”] = 5
除了可以通過 make 函式建立 map 外,還可以通過字面量的方式。同樣是上面的示例,用字面量的方式做如下操作:

nameAgeMap:=map[string]int{"Golang":5}

在建立 map 的同時新增鍵值對,如果不想新增鍵值對,使用空大括號 {} 即可,要注意的是,大括號一定不能省略。

Map 獲取和刪除

map 的操作和切片、陣列差不多,都是通過 [] 操作符,只不過陣列切片的 [] 中是索引,而 map 的 [] 中是 Key,如下面的程式碼所示:

//新增鍵值對或者更新對應 Key 的 Value
nameAgeMap["Golang"] = 5
//獲取指定 Key 對應的 Value
age:=nameAgeMap["Golang"]

Go 語言的 map 可以獲取不存在的 K-V 鍵值對,如果 Key 不存在,返回的 Value 是該型別的零值,比如 int 的零值就是 0。所以很多時候,我們需要先判斷 map 中的 Key 是否存在。

map 的 [] 操作符可以返回兩個值:

  • 第一個值是對應的 Value;
  • 第二個值標記該 Key 是否存在,如果存在,它的值為 true。

通過下面的程式碼進行演示:

nameAgeMap:=make(map[string]int)
nameAgeMap["Golang"] = 20
age,ok:=nameAgeMap["Golang1"]
if ok {
    fmt.Println(age)
}

在示例中,age 是返回的 Value,ok 用來標記該 Key 是否存在,如果存在則列印 age。

如果要刪除 map 中的鍵值對,使用內建的 delete 函式即可,比如要刪除 nameAgeMap 中 Key 為Golang的鍵值對。我們用下面的程式碼進行演示:

delete(nameAgeMap,"Golang")

delete 有兩個引數:第一個引數是 map,第二個引數是要刪除鍵值對的 Key。

遍歷 Map

map 是一個鍵值對集合,它同樣可以被遍歷,在 Go 語言中,map 的遍歷使用 for range 迴圈。

對於 map,for range 返回兩個值:

  • 第一個是 map 的 Key;
  • 第二個是 map 的 Value。

我們用下面的程式碼進行演示:

//測試 for range
nameAgeMap["飛雪無情"] = 20
nameAgeMap["飛雪無情1"] = 21
nameAgeMap["飛雪無情2"] = 22
for k,v:=range nameAgeMap{
    fmt.Println("Key is",k,",Value is",v)
}

需要注意的是 map 的遍歷是無序的,也就是說每次遍歷,鍵值對的順序可能會不一樣。如果想按順序遍歷,可以先獲取所有的 Key,並對 Key 排序,然後根據排序好的 Key 獲取對應的 Value。

小技巧:for range map 的時候,也可以使用一個值返回。使用一個返回值的時候,這個返回值預設是 map 的 Key。

Map 的大小

和陣列切片不一樣,map 是沒有容量的,它只有長度,也就是 map 的大小(鍵值對的個數)。要獲取 map 的大小,使用內建的 len 函式即可,如下程式碼所示:

fmt.Println(len(nameAgeMap))
String 和 []byte

字串 string 也是一個不可變的位元組序列,所以可以直接轉為位元組切片 []byte,如下面的程式碼所示:

s:="Hello飛雪無情"
bs:=[]byte(s)

string 不止可以直接轉為 []byte,還可以使用 [] 操作符獲取指定索引的位元組值,如以下示例:

s:="Hello飛雪無情"
bs:=[]byte(s)
fmt.Println(bs)
fmt.Println(s[0],s[1],s[15])

可能會有疑惑,在這個示例中,字串 s 裡的字母和中文加起來不是 9 個字元嗎?怎麼可以使用 s[15] 超過 9 的索引呢?其實恰恰就是因為字串是位元組序列,每一個索引對應的是一個位元組,而在 UTF8 編碼下,一個漢字對應三個位元組,所以字串 s 的長度其實是 17。

執行下面的程式碼,就可以看到列印的結果是 17。

fmt.Println(len(s))

如果想把一個漢字當成一個長度計算,可以使用 utf8.RuneCountInString 函式。執行下面的程式碼,可以看到列印結果是 9,也就是 9 個 unicode(utf8)字元,和看到的字元的個數一致。

fmt.Println(utf8.RuneCountInString(s))

而使用 for range 對字串進行迴圈時,也恰好是按照 unicode 字元進行迴圈的,所以對於字串 s 來說,迴圈了 9 次。

在下面示例的程式碼中,i 是索引,r 是 unicode 字元對應的 unicode 碼點,這也說明了 for range 迴圈在處理字串的時候,會自動地隱式解碼 unicode 字串。

for i,r:=range s{
    fmt.Println(i,r)
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結
?

相關文章