Go語(yǔ)言基礎(chǔ)學(xué)習(xí)之map的示例詳解
Map
map是一種無(wú)序的基于key-value的數(shù)據(jù)結(jié)構(gòu),Go語(yǔ)言中的map是引用類型,必須初始化才能使用。
map定義
Go語(yǔ)言中 map的定義語(yǔ)法如下
map[KeyType]ValueType
其中,
KeyType:表示鍵的類型。
ValueType:表示鍵對(duì)應(yīng)的值的類型。
map類型的變量默認(rèn)初始值為nil,需要使用make()函數(shù)來(lái)分配內(nèi)存。語(yǔ)法為:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,該參數(shù)雖然不是必須的,但是我們應(yīng)該在初始化map的時(shí)候就為其指定一個(gè)合適的容量。
map基本使用
map中的數(shù)據(jù)都是成對(duì)出現(xiàn)的,map的基本使用示例代碼如下:
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["張三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
輸出:
map[小明:100 張三:90]
100
type of a:map[string]int
map也支持在聲明的時(shí)候填充元素,例如:
func main() {
userInfo := map[string]string{
"username": "pprof.cn",
"password": "123456",
}
fmt.Println(userInfo) //
}
判斷某個(gè)鍵是否存在
Go語(yǔ)言中有個(gè)判斷map中鍵是否存在的特殊寫(xiě)法,格式如下:
value, ok := map[key]
舉個(gè)例子:
func main() {
scoreMap := make(map[string]int)
scoreMap["張三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok為true,v為對(duì)應(yīng)的值;不存在ok為false,v為值類型的零值
v, ok := scoreMap["張三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查無(wú)此人")
}
}
map的遍歷
Go語(yǔ)言中使用for range遍歷map。
func main() {
scoreMap := make(map[string]int)
scoreMap["張三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
但我們只想遍歷key的時(shí)候,可以按下面的寫(xiě)法:
func main() {
scoreMap := make(map[string]int)
scoreMap["張三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for k := range scoreMap {
fmt.Println(k)
}
}
注意: 遍歷map時(shí)的元素順序與添加鍵值對(duì)的順序無(wú)關(guān)。
使用delete()函數(shù)刪除鍵值對(duì)
使用delete()內(nèi)建函數(shù)從map中刪除一組鍵值對(duì),delete()函數(shù)的格式如下:
delete(map, key)
其中,
map:表示要?jiǎng)h除鍵值對(duì)的map
key:表示要?jiǎng)h除的鍵值對(duì)的鍵
示例代碼如下:
func main(){
scoreMap := make(map[string]int)
scoreMap["張三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
delete(scoreMap, "小明")//將小明:100從map中刪除
for k,v := range scoreMap{
fmt.Println(k, v)
}
}
按照指定順序遍歷map
func main() {
rand.Seed(time.Now().UnixNano()) //初始化隨機(jī)數(shù)種子
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu開(kāi)頭的字符串
value := rand.Intn(100) //生成0~99的隨機(jī)整數(shù)
scoreMap[key] = value
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//對(duì)切片進(jìn)行排序
sort.Strings(keys)
//按照排序后的key遍歷map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
元素為map類型的切片
下面的代碼演示了切片中的元素為map類型時(shí)的操作:
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 對(duì)切片中的map元素進(jìn)行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "王五"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "紅旗大街"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
值為切片類型的map
下面的代碼演示了map中值為切片類型的操作:
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中國(guó)"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
len(m)獲取map的長(zhǎng)度,go不支持對(duì)map上執(zhí)行cap函數(shù)。
map中的key可以是任意能夠用==操作符比較的類型,不能是函數(shù)、map、切片,以及包含上述3中類型成員變量的的struct。map的value可以是任意類型。
type f func(int) bool
type m map[int]byte
type s []int
type i int
var m1 map[i]f
fmt.Println(m1)
/** 函數(shù)、map、切片不能當(dāng)key **/
// var m2 map[f]bool
// fmt.Println(m2)
// var m3 map[m]bool
// fmt.Println(m3)
// var m4 map[s]bool
// fmt.Println(m4)
type user struct {
scores float32 //如果scores是slice,則user不能作為map的key
}
u := user{}
m5 := make(map[user]interface{})
m5[u] = 5
fmt.Println(m5)
Map實(shí)現(xiàn)原理
什么是Map
go map的底層實(shí)現(xiàn)是hash table,根據(jù)key查找value的時(shí)間復(fù)雜度是O(1)。

key與value存儲(chǔ)
最通俗的話說(shuō)Map是一種通過(guò)key來(lái)獲取value的一個(gè)數(shù)據(jù)結(jié)構(gòu),其底層存儲(chǔ)方式為數(shù)組,在存儲(chǔ)時(shí)key不能重復(fù),當(dāng)key重復(fù)時(shí),value進(jìn)行覆蓋,我們通過(guò)key進(jìn)行hash運(yùn)算(可以簡(jiǎn)單理解為把key轉(zhuǎn)化為一個(gè)整形數(shù)字)然后對(duì)數(shù)組的長(zhǎng)度取余,得到key存儲(chǔ)在數(shù)組的哪個(gè)下標(biāo)位置,最后將key和value組裝為一個(gè)結(jié)構(gòu)體,放入數(shù)組下標(biāo)處,看下圖:
length = len(array) = 4
hashkey1 = hash(xiaoming) = 4
index1 = hashkey1% length= 0
hashkey2 = hash(xiaoli) = 6
index2 = hashkey2% length= 2

hash沖突
如上圖所示,數(shù)組一個(gè)下標(biāo)處只能存儲(chǔ)一個(gè)元素,也就是說(shuō)一個(gè)數(shù)組下標(biāo)只能存儲(chǔ)一對(duì)key,value, hashkey(xiaoming)=4占用了下標(biāo)0的位置,假設(shè)我們遇到另一個(gè)key,hashkey(xiaowang)也是4,這就是hash沖突(不同的key經(jīng)過(guò)hash之后得到的值一樣),那么key=xiaowang的怎么存儲(chǔ)?
hash沖突的常見(jiàn)解決方法
開(kāi)放定址法:也就是說(shuō)當(dāng)我們存儲(chǔ)一個(gè)key,value時(shí),發(fā)現(xiàn)hashkey(key)的下標(biāo)已經(jīng)被別key占用,那我們?cè)谶@個(gè)數(shù)組中空間中重新找一個(gè)沒(méi)被占用的存儲(chǔ)這個(gè)沖突的key,那么沒(méi)被占用的有很多,找哪個(gè)好呢?常見(jiàn)的有線性探測(cè)法,線性補(bǔ)償探測(cè)法,隨機(jī)探測(cè)法,這里我們主要說(shuō)一下線性探測(cè)法
線性探測(cè),字面意思就是按照順序來(lái),從沖突的下標(biāo)處開(kāi)始往后探測(cè),到達(dá)數(shù)組末尾時(shí),從數(shù)組開(kāi)始處探測(cè),直到找到一個(gè)空位置存儲(chǔ)這個(gè)key,當(dāng)數(shù)組都找不到的情況下回?cái)U(kuò)容(事實(shí)上當(dāng)數(shù)組容量快滿的時(shí)候就會(huì)擴(kuò)容了);查找某一個(gè)key的時(shí)候,找到key對(duì)應(yīng)的下標(biāo),比較key是否相等,如果相等直接取出來(lái),否則按照順尋探測(cè)直到碰到一個(gè)空位置,說(shuō)明key不存在。如下圖:首先存儲(chǔ)key=xiaoming在下標(biāo)0處,當(dāng)存儲(chǔ)key=xiaowang時(shí),hash沖突了,按照線性探測(cè),存儲(chǔ)在下標(biāo)1處,(紅色的線是沖突或者下標(biāo)已經(jīng)被占用了) 再者key=xiaozhao存儲(chǔ)在下標(biāo)4處,當(dāng)存儲(chǔ)key=xiaoliu是,hash沖突了,按照線性探測(cè),從頭開(kāi)始,存儲(chǔ)在下標(biāo)2處 (黃色的是沖突或者下標(biāo)已經(jīng)被占用了)

拉鏈法:何為拉鏈,簡(jiǎn)單理解為鏈表,當(dāng)key的hash沖突時(shí),我們?cè)跊_突位置的元素上形成一個(gè)鏈表,通過(guò)指針互連接,當(dāng)查找時(shí),發(fā)現(xiàn)key沖突,順著鏈表一直往下找,直到鏈表的尾節(jié)點(diǎn),找不到則返回空,如下圖:

開(kāi)放定址(線性探測(cè))和拉鏈的優(yōu)缺點(diǎn)
- 由上面可以看出拉鏈法比線性探測(cè)處理簡(jiǎn)單
- 線性探測(cè)查找是會(huì)被拉鏈法會(huì)更消耗時(shí)間
- 線性探測(cè)會(huì)更加容易導(dǎo)致擴(kuò)容,而拉鏈不會(huì)
- 拉鏈存儲(chǔ)了指針,所以空間上會(huì)比線性探測(cè)占用多一點(diǎn)
- 拉鏈?zhǔn)莿?dòng)態(tài)申請(qǐng)存儲(chǔ)空間的,所以更適合鏈長(zhǎng)不確定的
Go中Map的使用
直接用代碼描述,直觀,簡(jiǎn)單,易理解
//直接創(chuàng)建初始化一個(gè)mao
var mapInit = map[string]string {"xiaoli":"湖南", "xiaoliu":"天津"}
//聲明一個(gè)map類型變量,
//map的key的類型是string,value的類型是string
var mapTemp map[string]string
//使用make函數(shù)初始化這個(gè)變量,并指定大小(也可以不指定)
mapTemp = make(map[string]string,10)
//存儲(chǔ)key ,value
mapTemp["xiaoming"] = "北京"
mapTemp["xiaowang"]= "河北"
//根據(jù)key獲取value,
//如果key存在,則ok是true,否則是flase
//v1用來(lái)接收key對(duì)應(yīng)的value,當(dāng)ok是false時(shí),v1是nil
v1,ok := mapTemp["xiaoming"]
fmt.Println(ok,v1)
//當(dāng)key=xiaowang存在時(shí)打印value
if v2,ok := mapTemp["xiaowang"]; ok{
fmt.Println(v2)
}
//遍歷map,打印key和value
for k,v := range mapTemp{
fmt.Println(k,v)
}
//刪除map中的key
delete(mapTemp,"xiaoming")
//獲取map的大小
l := len(mapTemp)
fmt.Println(l)
看了上面的map創(chuàng)建,初始化,增刪改查等操作,我們發(fā)現(xiàn)go的api其實(shí)挺簡(jiǎn)單易學(xué)的
Go中Map的實(shí)現(xiàn)原理
知其然,更得知其所以然,會(huì)使用map了,多問(wèn)問(wèn)為什么,go底層map到底怎么存儲(chǔ)呢?接下來(lái)我們一探究竟。map的源碼位于 src/runtime/map.go中 筆者go的版本是1.12在go中,map同樣也是數(shù)組存儲(chǔ)的的,每個(gè)數(shù)組下標(biāo)處存儲(chǔ)的是一個(gè)bucket,這個(gè)bucket的類型見(jiàn)下面代碼,每個(gè)bucket中可以存儲(chǔ)8個(gè)kv鍵值對(duì),當(dāng)每個(gè)bucket存儲(chǔ)的kv對(duì)到達(dá)8個(gè)之后,會(huì)通過(guò)overflow指針指向一個(gè)新的bucket,從而形成一個(gè)鏈表,看bmap的結(jié)構(gòu),我想大家應(yīng)該很納悶,沒(méi)看見(jiàn)kv的結(jié)構(gòu)和overflow指針啊,事實(shí)上,這兩個(gè)結(jié)構(gòu)體并沒(méi)有顯示定義,是通過(guò)指針運(yùn)算進(jìn)行訪問(wèn)的。
//bucket結(jié)構(gòu)體定義 b就是bucket
type bmap{
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
//翻譯:top hash通常包含該bucket中每個(gè)鍵的hash值的高八位。
如果tophash[0]小于mintophash,則tophash[0]為桶疏散狀態(tài) //bucketCnt 的初始值是8
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt values.
// NOTE: packing all the keys together and then all the values together makes the // code a bit more complicated than alternating key/value/key/value/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer. //翻譯:接下來(lái)是bucketcnt鍵,然后是bucketcnt值。
注意:將所有鍵打包在一起,然后將所有值打包在一起, 使得代碼比交替鍵/值/鍵/值/更復(fù)雜。但它允許//我們消除可能需要的填充, 例如map[int64]int8./后面跟一個(gè)溢出指針}
看上面代碼以及注釋,我們能得到bucket中存儲(chǔ)的kv是這樣的,tophash用來(lái)快速查找key值是否在該bucket中,而不同每次都通過(guò)真值進(jìn)行比較;還有kv的存放,為什么不是k1v1,k2v2…… 而是k1k2…v1v2…,我們看上面的注釋說(shuō)的 map[int64]int8,key是int64(8個(gè)字節(jié)),value是int8(一個(gè)字節(jié)),kv的長(zhǎng)度不同,如果按照kv格式存放,則考慮內(nèi)存對(duì)齊v也會(huì)占用int64,而按照后者存儲(chǔ)時(shí),8個(gè)v剛好占用一個(gè)int64,從這個(gè)就可以看出go的map設(shè)計(jì)之巧妙。

最后我們分析一下go的整體內(nèi)存結(jié)構(gòu),閱讀一下map存儲(chǔ)的源碼,如下圖所示,當(dāng)往map中存儲(chǔ)一個(gè)kv對(duì)時(shí),通過(guò)k獲取hash值,hash值的低八位和bucket數(shù)組長(zhǎng)度取余,定位到在數(shù)組中的那個(gè)下標(biāo),hash值的高八位存儲(chǔ)在bucket中的tophash中,用來(lái)快速判斷key是否存在,key和value的具體值則通過(guò)指針運(yùn)算存儲(chǔ),當(dāng)一個(gè)bucket滿時(shí),通過(guò)overfolw指針鏈接到下一個(gè)bucket。

go的map存儲(chǔ)源碼如下,省略了一些無(wú)關(guān)緊要的代碼
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
//獲取hash算法
alg := t.key.alg
//計(jì)算hash值
hash := alg.hash(key, uintptr(h.hash0))
//如果bucket數(shù)組一開(kāi)始為空,則初始化
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// 定位存儲(chǔ)在哪一個(gè)bucket中
bucket := hash & bucketMask(h.B)
//得到bucket的結(jié)構(gòu)體
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) +bucket*uintptr(t.bucketsize)))
//獲取高八位hash值
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var val unsafe.Pointer
bucketloop:
//死循環(huán)
for {
//循環(huán)bucket中的tophash數(shù)組
for i := uintptr(0); i < bucketCnt; i++ {
//如果hash不相等
if b.tophash[i] != top {
//判斷是否為空,為空則插入
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
val = add( unsafe.Pointer(b),
dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize) )
}
//插入成功,終止最外層循環(huán)
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
//到這里說(shuō)明高八位hash一樣,獲取已存在的key
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
//判斷兩個(gè)key是否相等,不相等就循環(huán)下一個(gè)
if !alg.equal(key, k) {
continue
}
// 如果相等則更新
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
//獲取已存在的value
val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
goto done
}
//如果上一個(gè)bucket沒(méi)能插入,則通過(guò)overflow獲取鏈表上的下一個(gè)bucket
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
if inserti == nil {
// all current buckets are full, allocate a new one.
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
val = add(insertk, bucketCnt*uintptr(t.keysize))
}
// store new key/value at insert position
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectvalue() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(val) = vmem
}
typedmemmove(t.key, insertk, key)
//將高八位hash值存儲(chǔ)
*inserti = top
h.count++
return val
}
到此這篇關(guān)于Go語(yǔ)言基礎(chǔ)學(xué)習(xí)之map的示例詳解的文章就介紹到這了,更多相關(guān)Go map內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言排序算法之插入排序與生成隨機(jī)數(shù)詳解
從這篇文章開(kāi)始將帶領(lǐng)大家學(xué)習(xí)Go語(yǔ)言的經(jīng)典排序算法,比如插入排序、選擇排序、冒泡排序、希爾排序、歸并排序、堆排序和快排,二分搜索,外部排序和MapReduce等,本文將先詳細(xì)介紹插入排序,并給大家分享了go語(yǔ)言生成隨機(jī)數(shù)的方法,下面來(lái)一起看看吧。2017-11-11
golang struct, map, json之間的相互轉(zhuǎn)換
本文用于記錄我在 golang 學(xué)習(xí)階段遇到的類型轉(zhuǎn)換問(wèn)題,針對(duì)的是 json 、map、struct 之間相互轉(zhuǎn)換的問(wèn)題,感興趣的可以了解一下2021-06-06
Go語(yǔ)言學(xué)習(xí)之函數(shù)的定義與使用詳解
這篇文章主要為大家詳細(xì)介紹Go語(yǔ)言中函數(shù)的定義與使用,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語(yǔ)言有一定幫助,需要的可以參考一下2022-04-04
go語(yǔ)言goto語(yǔ)句跳轉(zhuǎn)到指定的標(biāo)簽實(shí)現(xiàn)方法
這篇文章主要介紹了go語(yǔ)言goto語(yǔ)句跳轉(zhuǎn)到指定的標(biāo)簽實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
Go開(kāi)發(fā)go-optioner工具實(shí)現(xiàn)輕松生成函數(shù)選項(xiàng)模式代碼
go-optioner?是一個(gè)在?Go?代碼中生成函數(shù)選項(xiàng)模式代碼的工具,可以根據(jù)給定的結(jié)構(gòu)定義自動(dòng)生成相應(yīng)的選項(xiàng)代碼,下面就來(lái)聊聊go-optioner是如何使用的吧2023-07-07
詳解Go語(yǔ)言如何利用高階函數(shù)寫(xiě)出優(yōu)雅的代碼
高階函數(shù)(Hiher-order?Function)定義為:滿足下列條件之一的函數(shù):接收一個(gè)或多個(gè)函數(shù)作為參數(shù);返回值是一個(gè)函數(shù)。本文為大家介紹了如何利用高階函數(shù)寫(xiě)出優(yōu)雅的代碼,希望對(duì)大家有所幫助2023-01-01
Go位集合相關(guān)操作bitset庫(kù)安裝使用
這篇文章主要為大家介紹了Go位集合相關(guān)操作bitset庫(kù)安裝使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
golang 生成二維碼海報(bào)的實(shí)現(xiàn)代碼
這篇文章主要介紹了golang 生成二維碼海報(bào)的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02

