前言
哈希表是一種巧妙並且實用的數據結構。它是一個無序的 key/value對 的集合,其中所有的 key 都是不同的,然后通過給定的 key 可以在常數時間復雜度內檢索、更新或刪除對應的 value。
在 Go 語言中,一個 map 就是一個哈希表的引用,map 類型可以寫為 map[K]V,其中 K 和 V 分別對應 key 和 value。map 中所有的 key 都有相同的類型,所有的 value 也有着相同的類型,但是 key 和 value 之間可以是不同的數據類型。其中 K 對應的 key 必須是支持 == 比較運算符的數據類型(切片、函數等不支持),所以 map 可以通過測試 key 是否相等來判斷是否已經存在。雖然浮點數類型也是支持相等運算符比較的,但是將浮點數用做 key 類型則是一個壞的想法。對於 V 對應的 value 數據類型則沒有任何的限制。
- map 是無序的
- 在 Go 語言中的 map 是引用類型,必須初始化才能使用。
Map 是一種集合,所以我們可以像迭代數組和切片那樣迭代它。由於 map 是無序的,我們無法決定它的返回順序。
map 的定義
可以使用內建函數 make 也可以使用 map 關鍵字來定義 map:
// 使用 make 函數
m := make(map[keyType]valueType)
// 長度為 0 的 map
m := make(map[keyType]valueType, 0)
// 聲明變量,默認 map 是 nil
var m map[keyType]valueType
// 長度為 0 的 map
var m map[keyType]valueType{}
其中:
- m 為 map 的變量名。
- keyType 為鍵類型。
- valueType 是鍵對應的值類型。
在聲明的時候不需要知道 map 的長度,因為 map 是可以動態增長的。但是如果我們提前知道 map 需要的長度,最好指定一下。
我們可以用 len(m)
來查看 map 的長度。注意,使用 cap(m)
會報錯(cap 支持 數組、指向數組的指針、切片、channel):
invalid argument m (type map[string]int) for cap
如果不初始化 map,那么就會創建一個 nil map。nil map 不能用來存放鍵值對。如果向一個 nil 值的 map 存入元素將導致一個 panic 異常:
下面我們用 make 函數創建一個 map:
ages := make(map[string]int)
當然,我們也可以直接創建一個 map 並且指定一些最初的值:
ages := map[string]int{
"Conan": 18,
"Kidd": 23,
}
這種就相當於:
ages := make(map[string]int)
ages["Conan"] = 18
ages["Kidd"] = 23
所以,另一種創建空(不是 nil)的 map 方法是:
ages := map[string]int{}
map 在定義時,key 是唯一的,不允許重復(value 可以重復)。下面的程序會報錯:
ages := map[string]int{
"Conan": 18,
"Conan": 23,
}
但是之后在對 map 賦值時,則會覆蓋原來的 value
ages["Conan"] = 18
ages["Conan"] = 23
fmt.Println(ages["Conan"]) // 23
map 類型的零值是 nil,也就是沒有引用任何哈希表,其長度也為 0.
var ages map[string]int
fmt.Println(ages == nil) // true
fmt.Println(len(ages)) // 0
map 的基本使用
增
增加 map 的值很簡單,只需要 m[key] = value
即可,比如:
ages := make(map[string]int)
ages["Conan"] = 18
ages["Kidd"] = 23
刪
使用內置的 delete 函數可以刪除元素,參數為 map 和其對應的 key,沒有返回值:
delete(ages, "Conan")
注意:即使這些 key 不在 map 中也不會報錯。
改
修改 map 的內容和 增 的寫法類似,只不過 key 是已存在的,如果不存在,則為增加,例如:
ages := map[string]int{
"Conan": 18,
"Kidd": 23,
}
ages["Conan"] = 21
查
map 中的元素通過 key 對應的下標語法訪問:
ages["Conan"] = 18
fmt.Println(ages["Conan"]) // 18
要想遍歷 map 中全部的鍵值對的話,可以使用 range 風格的 for 循環實現,和之前的 slice 遍歷語法類似。例如:
for key, value := range ages {
fmt.Println(key, value)
}
如果用不到 value,無需使用匿名變量 _
,直接不寫即可:
for key := range ages {
fmt.Println(key)
}
如果查找失敗也沒有關系,程序也不會報錯,而是返回 value 類型對應的零值。例如:
ages := map[string]int{
"Conan": 18,
"Kidd": 23,
}
fmt.Println(ages["Lan"]) // 0
通過 key 作為索引下標來訪問 map 將產生一個 value。如果 key 在 map 中是存在的,那么將得到與 key 對應的 value;如果 key 不存在,那么將得到 value 對應類型的零值。
但是有時候我們需要知道對應的元素是否真的是在 map 之中。比如,如果元素類型是一個數字,你需要區分一個已經存在的 0,和不存在而返回零值的 0。例如:
ages := map[string]int{
"Conan": 18,
"Kidd": 23,
}
// 如果 key 存在,則 ok = true;不存在,ok = false
if value, ok := ages["Conan"]; ok {
fmt.Println(value)
} else {
fmt.Println("key 不存在")
}
在這種場景下,map 的下標語法將產生兩個值;第二個是一個布爾值,用於報告元素是否真的存在。布爾變量一般命名為 ok,特別適合馬上用於 if 條件判斷部分。
排
map 的迭代順序是不確定的。有沒有什么辦法可以順序的打印出 map 呢?我們可以借助切片來完成。先將 key(或者 value)添加到一個切片中,再對切片排序,然后使用 for-range 方法打印出所有的 key 和 value。如下所示:
package main
import (
"fmt"
"sort"
)
func main() {
// 創建一個 ages map,並給三個值
ages := make(map[string]int)
ages["Conan"] = 18
ages["Kidd"] = 23
ages["Lan"] = 19
// 創建一個切片用於給 key 進行排序
var names []string
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
// 循環打印出 map 中的值
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
}
因為我們一開始就知道 names 的最終大小,因此給切片分配一個合適的容量大小將會更有效。下面的代碼創建了一個空的切片,但是切片的容量剛好可以放下 map 中全部的 key:
names := make([]string, 0, len(ages))
當然,如果使用結構體切片,這樣就會更有效:
type name struct {
key string
value int
}
比
map 之間不能進行相等比較;唯一的例外是和 nil 進行比較。要判斷兩個 map 是否包含相同的 key 和 value,我們必須通過一個循環實現:
func equalMap(x, y map[string]int) bool {
// 長度不一樣,肯定不相等
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || xv != yv {
return false
}
}
return true
}
map 作為函數參數
map 作為函數參數是地址傳遞(引用傳遞),作返回值時也一樣。
在函數內部對 map 進行操作,會影響主調函數中實參的值。例如:
func foo(m map[string]int) {
m["Conan"] = 22
m["Lan"] = 21
}
func main() {
m := make(map[string]int, 2)
m["Conan"] = 18
fmt.Println(m) // map[Conan:18]
foo(m)
fmt.Println(m) // map[Conan:22 Lan:21]
}
並發環境中使用的 map:sync.Map
Go 語言中的 map 在並發情況下,只讀是線程安全的,同時讀寫是線程不安全的。
下面我們來看一下在並發情況下讀寫 map 時會出現的問題,代碼如下:
// 創建一個 map
m := make(map[int]int)
// 開啟一個 go 程
go func () {
// 不停地對 map 進行寫入
for true {
m[1] = 1
}
}()
// 開啟一個 go 程
go func() {
// 不停的對 map 進行讀取
for true {
_ = m[1]
}
}()
// 運行 10 秒停止
time.Sleep(time.Second * 10)
運行代碼會報錯,錯誤如下:
fatal error: concurrent map read and map write
當兩個並發函數不斷地對 map 進行讀和寫時,map 內部會對這種並發操作進行檢查並提前發現。
當我們需要並發讀寫時,一般的做法是加鎖,但是這樣性能不高。
Go 語言在 1.9 版本中提供了一種效率較高的並發安全的 sync.Map。
sync.Map 有以下特性:
- 無須初始化,直接聲明即可
- sync.Map 不能使用 map 的方式進行取值和設置等操作,而是使用 sync.Map 的方法進行調用:Store 表示存儲,Load 表示獲取,Delete 表示刪除。
- 使用 Range 配合一個回調函數進行遍歷操作,通過回調函數返回內部遍歷出來的值,Range 參數中回調函數的返回值在需要繼續迭代遍歷時返回 true,終止迭代遍歷時,返回 false。
並發安全的 sync.Map 示例代碼如下:
package main
import (
"fmt"
"sync"
)
func main() {
var ages sync.Map
// 將鍵值對保存到 sync.Map
ages.Store("Conan", 18)
ages.Store("Kidd", 23)
ages.Store("Lan", 18)
// 從 sync.Map 中根據鍵取值
age, ok := ages.Load("Conan")
fmt.Println(age, ok)
// 根據鍵刪除對應的鍵值對
ages.Delete("Kidd")
fmt.Println("刪除后的 sync.Map: ", ages)
// 遍歷所有 sync.Map 中的鍵值對
ages.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true
})
}
sync.Map 沒有提供獲取 map 數量的方法,替代方法是在獲取 sync.Map 時遍歷自行計算數量,sync.Map 為了保證並發安全有一些性能損失,因此在非並發情況下,使用 map 相比使用 sync.Map 會有更好的性能。
所以,我們用 sync.Map 時進行同時讀寫是沒問題的,示例代碼如下:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.Map
// 開啟一個 go 程
go func() {
// 不停地對 map 進行寫入
for true {
m.Store(1, 1)
}
}()
// 開啟一個 go 程
go func() {
// 不停的對 map 進行讀取並打印讀取結果
for true {
value, _ := m.Load(1)
fmt.Println(value)
}
}()
time.Sleep(time.Second * 10)
}
這時的結果就會一直輸出 1。
練習
1、封裝 wordCountFunc() 函數。接收一段英文字符串 str。返回一個 map,記錄 str 中每個“單詞”出現的次數。
示例:
輸入:"I love my work and I love my family too"
輸出:
family:1
too:1
I:2
love:2
my:2
work:1
and:1
提示:使用 strings.Fields() 函數可提高效率
實現:
package main
import (
"fmt"
"strings"
)
func wordCountFunc(str string) map[string]int {
// 使用 strings.Fields 進行拆分, 自動按照空格對字符串進行拆分成切片
wordSlice := strings.Fields(str)
// 創建一個用於存儲 word 次數的 map
m := make(map[string]int)
// 遍歷拆分后的字符串切片
for _, value := range wordSlice {
if _, ok := m[value]; !ok {
// key 不存在
m[value] = 1
} else {
// key 值已存在
m[value]++
}
}
return m
}
func main() {
str := "I love my work and I love my family too"
res := wordCountFunc(str)
// 遍歷 map, 展示每個 word 出現的次數
for key, value := range res {
fmt.Println(key, ": ", value)
}
}
如需更深入的了解 map 的原理,推薦閱讀這篇文章:深度解密Go語言之map
李培冠博客
歡迎訪問我的個人網站:
李培冠博客:lpgit.com