哈希表是一種巧妙而且實用的數據結構。它是一個無序的 key/value對 的集合,其中全部的 key 都是不一樣的,而後經過給定的 key 能夠在常數時間複雜度內檢索、更新或刪除對應的 value。html
在 Go 語言中,一個 map 就是一個哈希表的引用,map 類型能夠寫爲 map[K]V,其中 K 和 V 分別對應 key 和 value。map 中全部的 key 都有相同的類型,全部的 value 也有着相同的類型,可是 key 和 value 之間能夠是不一樣的數據類型。其中 K 對應的 key 必須是支持 == 比較運算符的數據類型(切片、函數等不支持),因此 map 能夠經過測試 key 是否相等來判斷是否已經存在。雖然浮點數類型也是支持相等運算符比較的,可是將浮點數用作 key 類型則是一個壞的想法。對於 V 對應的 value 數據類型則沒有任何的限制。git
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{}
其中:數據結構
在聲明的時候不須要知道 map 的長度,由於 map 是能夠動態增加的。可是若是咱們提早知道 map 須要的長度,最好指定一下。併發
咱們能夠用 len(m)
來查看 map 的長度。注意,使用 cap(m)
會報錯(cap 支持 數組、指向數組的指針、切片、channel):app
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 的值很簡單,只須要 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 進行操做,會影響主調函數中實參的值。例如:
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] }
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 示例代碼以下:
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。
一、封裝 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