《快學 Go 語言》第 6 課 —— 字典

老錢發表於2018-11-16

字典在數學上的詞彙是對映,將一個集合中的所有元素關聯到另一個集合中的部分或全部元素,並且只能是一一對映或者多對一對映。

《快學 Go 語言》第 6 課 —— 字典

陣列切片讓我們具備了可以操作一塊連續記憶體的能力,它是對同質元素的統一管理。而字典則賦予了不連續不同類的記憶體變數的關聯性,它表達的是一種因果關係,字典的 key 是因,字典的 value 是果。如果說陣列和切片賦予了我們步行的能力,那麼字典則讓我們具備了跳躍的能力。

指標、陣列切片和字典都是容器型變數,字典比陣列切片在使用上要簡單很多,但是內部結構卻無比複雜。本節我們只專注字典的基礎使用,在後續的高階章節再來分析它的內部結構。

字典的建立

關於 Go 語言有很多批評的聲音,比如說它不支援範型。其實嚴格來說 Go 是支援範型的,只不過很弱,範型在 Go 語言裡是一種很弱的存在。比如陣列切片和字典型別都是支援範型的。在建立字典時,必須要給 key 和 value 指定型別。建立字典也可以使用 make 函式

package main

import "fmt"

func main() {
	var m map[int]string = make(map[int]string)
	fmt.Println(m, len(m))
}

----------
map[] 0
複製程式碼

使用 make 函式建立的字典是空的,長度為零,內部沒有任何元素。如果需要給字典提供初始化的元素,就需要使用另一種建立字典的方式。

package main

import "fmt"

func main() {
	var m map[int]string = map[int]string{
		90: "優秀",
		80: "良好",
		60: "及格",  // 注意這裡逗號不可缺少,否則會報語法錯誤
	}
	fmt.Println(m, len(m))
}

---------------
map[90:優秀 80:良好 60:及格] 3
複製程式碼

字典變數同樣支援型別推導,上面的變數定義可以簡寫成

var m = map[int]string{
 90: "優秀",
 80: "良好",
 60: "及格",
}
複製程式碼

如果你可以預知字典內部鍵值對的數量,那麼還可以給 make 函式傳遞一個整數值,通知執行時提前分配好相應的記憶體。這樣可以避免字典在長大的過程中要經歷的多次擴容操作。

var m = make(map[int]string, 16)
複製程式碼

字典的讀寫

同 Python 語言一樣,字典可以使用中括號來讀寫內部元素,使用 delete 函式來刪除元素。

package main

import "fmt"

func main() {
	var fruits = map[string]int {
		"apple": 2,
		"banana": 5,
		"orange": 8,
	}
	// 讀取元素
	var score = fruits["banana"]
	fmt.Println(score)
	
	// 增加或修改元素
	fruits["pear"] = 3
	fmt.Println(fruits)
	
	// 刪除元素
	delete(fruits, "pear")
	fmt.Println(fruits)
}

-----------------------
5
map[apple:2 banana:5 orange:8 pear:3]
map[orange:8 apple:2 banana:5]

複製程式碼

字典 key 不存在會怎樣?

刪除操作時,如果對應的 key 不存在,delete 函式會靜默處理。遺憾的是 delete 函式沒有返回值,你無法直接得到 delete 操作是否真的刪除了某個元素。你需要通過長度資訊或者提前嘗試讀取 key 對應的 value 來得知。

讀操作時,如果 key 不存在,也不會丟擲異常。它會返回 value 型別對應的零值。如果是字串,對應的零值是空串,如果是整數,對應的零值是 0,如果是布林型,對應的零值是 false。

你不能通過返回的結果是否是零值來判斷對應的 key 是否存在,因為 key 對應的 value 值可能恰好就是零值,比如下面的字典你就不能判斷 "durin" 是否存在

var m = map[string]int {
  "durin": 0  // 舉個栗子而已,其實我還是喜歡吃榴蓮的
}
複製程式碼

這時候必須使用字典的特殊語法,如下

package main

import "fmt"

func main() {
	var fruits = map[string]int {
		"apple": 2,
		"banana": 5,
		"orange": 8,
	}

	var score, ok = fruits["durin"]
	if ok {
		fmt.Println(score)
	} else {
		fmt.Println("durin not exists")
	}

	fruits["durin"] = 0
	score, ok = fruits["durin"]
	if ok {
		fmt.Println(score)
	} else {
		fmt.Println("durin still not exists")
	}
}

-------------
durin not exists
0
複製程式碼

字典的下標讀取可以返回兩個值,使用第二個返回值都表示對應的 key 是否存在。初學者看到這種奇怪的用法是需要花時間來消化的,讀者不需要想太多,它只是 Go 語言提供的語法糖,內部並沒有太多的玄妙。正常的函式呼叫可以返回多個值,但是並不具備這種“隨機應變”的特殊能力 —— 「多型返回值」。

字典的遍歷

字典的遍歷提供了下面兩種方式,一種是需要攜帶 value,另一種是隻需要 key,需要使用到 Go 語言的 range 關鍵字。

package main

import "fmt"

func main() {
	var fruits = map[string]int {
		"apple": 2,
		"banana": 5,
		"orange": 8,
	}

	for name, score := range fruits {
		fmt.Println(name, score)
	}

	for name := range fruits {
		fmt.Println(name)
	}
}

------------
orange 8
apple 2
banana 5
apple
banana
orange
複製程式碼

奇怪的是,Go 語言的字典沒有提供諸於 keys() 和 values() 這樣的方法,意味著如果你要獲取 key 列表,就得自己迴圈一下,如下

package main

import "fmt"

func main() {
	var fruits = map[string]int {
		"apple": 2,
		"banana": 5,
		"orange": 8,
	}

	var names = make([]string, 0, len(fruits))
	var scores = make([]int, 0, len(fruits))

	for name, score := range fruits {
		names = append(names, name)
		scores = append(scores, score)
	}

	fmt.Println(names, scores)
}

----------
[apple banana orange] [2 5 8]
複製程式碼

這會讓程式碼寫起來比較繁瑣,不過 Go 語言官方就是沒有提供,讀者還是努力習慣一下吧

執行緒(協程)安全

Go 語言的內建字典不是執行緒安全的,如果需要執行緒安全,必須使用鎖來控制。在後續鎖的章節裡,我們將會自己實現一個執行緒安全的字典。

字典變數裡存的是什麼?

字典變數裡存的只是一個地址指標,這個指標指向字典的頭部物件。所以字典變數佔用的空間是一個字,也就是一個指標的大小,64 位機器是 8 位元組,32 位機器是 4 位元組。

《快學 Go 語言》第 6 課 —— 字典

可以使用 unsafe 包提供的 Sizeof 函式來計算一個變數的大小

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var m = map[string]int{
		"apple":  2,
		"pear":   3,
		"banana": 5,
	}
	fmt.Println(unsafe.Sizeof(m))
}

------
8
複製程式碼

思考題

在遍歷字典得到 keys 和 values 的例子裡,我們分配了 names 和 scores 兩個切片,如果把程式碼片斷調整成下面這樣,會有什麼問題?

var names = make([]string, len(fruits))
var scores = make([]int, len(fruits))
複製程式碼

《快學 Go 語言》第 6 課 —— 字典

掃一掃二維碼閱讀《快學 Go 語言》更多章節

相關文章