Go語言map

itbsl發表於2018-11-30

map 是一種特殊的資料結構:一種元素對(pair)的無序集合,pair 的一個元素是 key,對應的另一個元素是 value,所以這個結構也稱為關聯陣列或字典。這是一種快速尋找值的理想結構:給定 key,對應的value 可以迅速定位。

內部實現

Map是給予雜湊表來實現,就是我們常說的Hash表,所以我們每次迭代Map的時候,列印的Key和Value是無序的,每次迭代的都不一樣,即使我們按照一定的順序存在也不行。

Map的雜湊表包含一組桶,每次儲存和查詢鍵值對的時候,都要先選擇一個桶。如何選擇桶呢?就是把指定的鍵傳給雜湊函式,就可以索引到相應的桶了,進而找到對應的鍵值。

這種方式的好處在於,儲存的資料越多,索引分佈越均勻,所以我們訪問鍵值對的速度也就越快,當然儲存的細節還有很多,大家可以參考Hash相關的知識,這裡我們只要記住Map儲存的是無序的鍵值對集合

宣告map

map 是引用型別,可以使用如下宣告:

var variableName map[keyType]valueType

[keyType]valueType之間允許有空格,但是gofmt移除了空格

在宣告的時候不需要知道 map 的長度,map 是可以動態增長的。

未初始化的 map 的值是 nil。

key 可以是任意可以用 == 或者 != 操作符比較的型別,比如 string、int、float。所以陣列、切片和結構體不能作為 key,但是指標和介面型別可以。如果要用結構體作為 key 可以提供 Key() 和 Hash() 方法,這樣可以通過結構體的域計算出唯一的數字或者字串的 key。

value 可以是任意型別的;通過使用空介面型別,我們可以儲存任意值,但是使用這種型別作為值時需要先做一次型別斷言。

map 傳遞給函式的代價很小: 在 32 位機器上佔 4 個位元組,64 位機器上佔 8 個位元組,無論實際上儲存了多少資料。通過 key 在 map 中尋找值是很快的,比線性查詢快得多,但是仍然比從陣列和切片的索引中直接讀取要慢 100 倍;所以如果你很在乎效能的話還是建議用切片來解決問題。

示例:

var map1 map[string]int

初始化map

Map的建立有make函式,Map字面量。make函式我們用它建立過切片,除此之外,它還可以用來建立Map。

map 是引用型別的: 記憶體用 make 方法來分配。

dict := make(map[string]int)

示例中建立了一個鍵型別為string的,值型別為int的map。現在建立好之後,這個map是空的,裡面什麼都沒有,我們給儲存一個鍵值對。

dict := make(map[string]int)
dict["itbsl"] = 25

儲存了一個Key為itbsl的,Value為25的鍵值對資料。

此外還有一種使用map字面量的方式建立和初始化map,對於上面的例子,我們可以同等實現。

dict := map[string]int{"itbsl": 25}

使用一個大括號進行初始化,鍵值對通過:分開,如果要同時初始化多個鍵值對,使用逗號分割。

dict := map[string]int{"itbsl":25,"kevin": 50}

當然我們可以不指定任何鍵值對,也就是一個空map。

dict := map[string]int{}

不管怎麼樣,使用map的字面量建立一定要帶上大括號。如果我們要建立一個nil的Map怎麼做呢?nil的Map是未初始化的,所以我們可以只宣告一個變數,既不能使用map字面量,也不能使用make函式分配記憶體。

var dict map[string]int

這樣就好了,但是這樣我們是不能操作儲存鍵值對的,必須要初始化後才可以,比如使用make函式,為其開啟一塊可以儲存資料的記憶體,也就是初始化。

var dict map[string]int
dict = make(map[string]int)
dict["itbsl"] = 25
fmt.Println(dict)

使用Map

Map的使用很簡單,和陣列切片差不多,陣列切片是使用索引,Map是通過鍵。

dict := make(map[string]int)
dict["itbsl"] = 25

以上示例,如果鍵itbsl存在,則對其值修改,如果不存在,則新增這個鍵值對。

獲取一個Map鍵的值也很簡單,和儲存差不多,還是給予上面的例子。

age := dict["itbsl"]

在Go Map中,如果我們獲取一個不存在的鍵的值,也是可以的,返回的是值型別的零值,這樣就會導致我們不知道是真的存在一個為零值的鍵值對呢,還是說這個鍵值對就不存在。對此,Map為我們提供了檢測一個鍵值對是否存在的方法。

age,exists := dict["itbsl"]

看這個例子,和獲取鍵的值沒有太大區別,只是多了一個返回值。第一個返回值是鍵的值;第二個返回值標記這個鍵是否存在,這是一個boolean型別的變數,我們判斷它就知道該鍵是否存在了。這也是Go多值返回的好處。

如果我們想刪除一個Map中的鍵值對,可以使用Go內建的delete函式。

delete(dict,"itbsl")

delete函式接受兩個引數,第一個是要操作的Map,第二個是要刪除的Map的鍵。

delete函式刪除不存在的鍵也是可以的,只是沒有任何作用。

想要遍歷Map的話,可以使用for range風格的迴圈,和遍歷切片一樣。

dict := map[string]int{"itbsl": 25}
for key, value := range dict {
    fmt.Println(key, value)
}

這裡的range 返回兩個值,第一個是Map的鍵,第二個是Map的鍵對應的值。這裡再次強調,這種遍歷是無序的,也就是鍵值對不會按既定的資料出現,如果想安順序遍歷,可以先對Map中的鍵排序,然後遍歷排序好的鍵,把對應的值取出來,下面看個例子就明白了。

func main() {
    dict := map[string]int{"kevin": 40, "itbsl": 25}
    var names []string
    for name := range dict {
        names = append(names, name)
    }
    sort.Strings(names) //排序
    for _, key := range names {
        fmt.Println(key, dict[key])
    }
}

這個例子裡有個技巧,range 一個Map的時候,也可以使用一個返回值,這個預設的返回值就是Map的鍵。

map容量

和陣列不同,map 可以根據新增的 key-value 對動態的伸縮,因此它不存在固定長度或者最大限制。但是你也可以選擇標明 map 的初始容量 capacity ,就像這樣: make(map[keytype]valuetype, cap) 。例如:

map2 := make(map[string]float, 100)

當 map 增長到容量上限的時候,如果再增加新的 key-value 對,map 的大小會自動加 1。所以出於效能的考慮,對於大的 map 或者會快速擴張的 map,即使只是大概知道容量,也最好先標明。

map的排序

map 預設是無序的,不管是按照 key 還是按照 value 預設都不排序。

如果你想為 map 排序,需要將 key(或者 value)拷貝到一個切片,再對切片排序,然後可以使用切片的 for-range 方法列印出所有的 key 和 value。

在函式間傳遞Map

函式間傳遞Map是不會拷貝一個該Map的副本的,也就是說如果一個Map傳遞給一個函式,該函式對這個Map做了修改,那麼這個Map的所有引用,都會感知到這個修改。

func main() {
    dict := map[string]int{"kevin": 40, "itbsl": 25}
    modify(dict)
    fmt.Println(dict["kevin"])
    fmt.Printf("main函式dict的地址為:   %p
", dict)
}

func modify(dict map[string]int) {
    dict["kevin"] = 10
    fmt.Printf("modify函式dict的地址為: %p
", dict)
}

輸出結果為:

modify函式dict的地址為: 0xc000076120
10
main函式dict的地址為:   0xc000076120

上面這個例子輸出的結果是10,也就是說已經被函式給修改了,可以證明傳遞的並不是一個Map的副本。這個特性和切片是類似的,這樣就會更高,因為複製整個Map的代價太大了。

相關文章