Golang 引用型別-map
1. 基本操作
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
m["c"] = 3
// ok-idiom
if v, ok := m["d"]; ok {
fmt.Println(v)
}
delete(m, "b")
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
}
2. key的型別
- 不可變型別:bool, number, string, ptr, channel, interface, struct, array(必須由不可變型別組成),可hash,支援==, !=操作
- 可被型別:slice, map, function等。
map的key必須是“不可變型別”,比如math.NaN() == math.NaN()
返回false,無法作為map的key!
浮點數做key值,需要考慮它的bit值是否一致:
func main() {
a := 3.1
b := 3.100000000001
c := 3.1000000000000000000000001
m := make(map[float64]int)
m[a] = 10
fmt.Println(math.Float64bits(a) == math.Float64bits(b)) // false
fmt.Println(math.Float64bits(a) == math.Float64bits(c)) // true
fmt.Println(m[a], m[b], m[c]) // 10, 0, 10
}
3. key是無序的
map擴容後,會發生key的搬遷,原來落在同一個bucket中的key,搬遷後,可能到了其他bucket上(當前bucket序號加上2^B)。
map的遍歷,按順序遍歷bucket,同時按順序遍歷bucket中的key。搬遷後,key位置發生變化,map也就不能按原來的順序了。
go在遍歷map時,並不固定從0號bucket開始,每次都從一個隨機的bucket開始遍歷,並且是從這個bucket的一個隨機號的cell開始遍歷。這樣,即使你寫死一個map,每次遍歷的結果都會不一致
4. 修改成員值
字典被設計成“not addressable”,不能直接修改value成員(結構體或陣列)
無法對 map 的 key 或 value 進行取址
即使通過unsafe.Pointer 等獲取到了 key 或 value 的地址,也不能長期持有,因為一旦發生擴容,key 和 value 的位置就會改變,之前儲存的地址也就失效了
func main() {
m := map[int]user{
1: user{"jackson", 21},
2: user{"sara", 20},
}
// cannot assign to struct field in a map
//m[1].age++
jackson := m[1]
jackson.age++
m[1] = jackson // 值拷貝,必須重新賦值
for k, v := range m {
fmt.Printf("%v: %v\n", k, v)
}
// 推薦使用指標
m2 := map[int]*user{
1: &user{"jackson", 21},
2: &user{"sara", 20},
}
m2[1].age++
for k, v := range m2 {
fmt.Printf("%v: %v\n", k, v)
}
}
func main() {
m := map[string][2]int{
"a": {1, 2},
}
// 陣列必須 addressable
//s := m["a"][:]
a := m["a"]
fmt.Printf("%p, %v\n", &a, a)
s := a[:]
fmt.Printf("%p, %v\n", &s, s)
}
5. map排序
func main() {
m := map[int]string{2: "b", 5: "e", 1: "a", 3: "c", 4: "d"}
s := make([]int, 0)
for k := range m {
s = append(s, k)
}
fmt.Println(s)
// 索引排序
sort.Ints(s)
for _, k := range s {
fmt.Printf("%v: %v\n", k, m[k])
}
}
6. map 執行緒安全
map執行緒不安全
在查詢、賦值、遍歷、刪除的過程中,都會檢測寫標誌,一旦發現寫標識位(等於1),則直接panic。賦值和刪除函式載檢測寫標識復位後,先將寫標識復位,才會進行之後的操作。
// 檢測寫標識:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
// 設定寫標識:
h.flags |= hashWriting
func main() {
var lock sync.RWMutex
m := make(map[string]int)
go func() {
rand.Seed(time.Now().UnixNano())
for {
lock.Lock()
m["a"] = rand.Intn(100)
lock.Unlock()
time.Sleep(time.Second)
}
}()
go func() {
for {
lock.Lock()
if v, ok := m["a"]; ok {
fmt.Printf("%v ", v)
}
lock.Unlock()
time.Sleep(time.Second)
}
}()
select {
case <-time.After(5 * time.Second):
return
}
}
7. map 刪除
底層執行的刪除函式:func mapdelete(t *maptype, h *hmap, key unsafe.Pointer)
mapdelete函式,它首先會檢測h.flags標識,如果發現寫標識為1,說明其他協程正在進行寫操作,直接panic
計算key的hash值,找到落入的bucket,檢查此map如果正進行擴容,直接觸發一次遷移操作
刪除操作同樣兩層迴圈,核心還是找到key的具體位置。尋找過程是類似的,在bucket中挨個cell尋找
// 對 key 清零
if t.indirectkey {
*(*unsafe.Pointer)(k) = nil
} else {
typedmemclr(t.key, k)
}
// 對 value 清零
if t.indirectvalue {
*(*unsafe.Pointer)(v) = nil
} else {
typedmemclr(t.elem, v)
}
最後,將count的值減1,將對應位置的tophash值換成Empty
可以邊遍歷邊刪除嗎?
map 並不是一個執行緒安全的資料結構。同時讀寫一個 map 是未定義的行為,如果被檢測到,會直接 panic。
一般而言,這可以通過讀寫鎖來解決:sync.RWMutex
。
讀之前呼叫 RLock()
函式,讀完之後呼叫 RUnlock()
函式解鎖;寫之前呼叫 Lock()
函式,寫完之後,呼叫 Unlock()
解鎖。
另外,sync.Map
是執行緒安全的 map,也可以使用。
8. map底層實現
- 雜湊查詢表(Hash table)
雜湊查詢表用一個雜湊函式將key分配到不同的bucket上,開銷主要在雜湊函式的計算及陣列的常數訪問時間,多種場景下,雜湊查詢效能更高。
雜湊查詢表一般會存在“碰撞”問題,就是說不同的key被雜湊到了同一個bucket上。一般有兩種應對方法:連結串列法 和 開發地址法。連結串列法:將一個bucket實現一個連結串列,落在同一個bucket中的key都會插入這個連結串列。開發地址法:碰撞發生後,通過一定的規則,在陣列的後面挑選“空位”,用來放置新的key。
- 搜素樹 (Search tree)
搜尋數法一般採用自平衡搜尋數,包括:AVL樹,紅黑樹。
自平衡搜尋樹法最差搜尋效率是O(logN),而雜湊查詢表最差是O(N)。
9. map 比較
map 只允許與nil比較
map深度相等(reflect.DeepEqual(
)的條件:
- 都為nil
- 非空、長度相等,指向同一個map實體物件 (賦值拷貝)
- 相應的key指向的value “深度“相等
func main() {
var m1 map[int]string
m2 := map[int]string{1: "a"}
m3 := m1
fmt.Println(m1 == nil) // true
fmt.Println(m2 == nil) // false
fmt.Println(m3 == nil) // true
//fmt.Println(m1 == m2) // 編譯失敗 map can only be compared to nil
//fmt.Println(m1 == m3) // same above
fmt.Println(reflect.DeepEqual(m1, m2)) // false
fmt.Println(reflect.DeepEqual(m1, m3)) // true
}
相關文章
- golang中 值型別,指標,引用的區別Golang型別指標
- Golang的值型別和引用型別的範圍、儲存區域、區別Golang型別
- 值型別和引用型別型別
- JavaScript引用型別-Object型別JavaScript型別Object
- js引用型別JS型別
- javascript:引用型別JavaScript型別
- c#:值型別&引用型別C#型別
- JavaScript值型別和引用型別JavaScript型別
- ECMAScript 原始型別與引用型別型別
- 值型別與引用型別的區別型別
- js基本型別和引用型別區別JS型別
- JavaScript - 基本型別與引用型別值JavaScript型別
- Java的基本型別和引用型別Java型別
- Go 的引用型別Go型別
- 引用型別之Object型別Object
- JS篇-基本型別和引用型別、typeofJS型別
- c#中值型別和引用型別的區別C#型別
- Java引用型別與WeakHashMapJava型別HashMap
- Java引用型別原理剖析Java型別
- 區別值型別資料和引用型別資料型別
- Java引用型別解析:掌握強引用、軟引用、弱引用和幻象引用的妙用Java型別
- 基本資料型別與API引用型別的使用資料型別API
- C++ 引用型別簡介C++型別
- 引用型別的深拷貝型別
- C++引用型別詳解C++型別
- 從賦值看基本型別和引用型別的區別賦值型別
- js資料型別之基本資料型別和引用資料型別JS資料型別
- JAVA中基本資料型別和引用資料型別Java資料型別
- golang的型別轉換Golang型別
- golang拾遺:嵌入型別Golang型別
- C#程式設計引用型別和值型別 以及引用傳遞和值傳遞C#程式設計型別
- map 型別 - Go 學習記錄型別Go
- 關於 Go 中 Map 型別和 Slice 型別的傳遞Go型別
- 09-引用型別-1-切片型別
- C#8.0 可空引用型別C#型別
- java的四大引用型別Java型別
- php中物件是引用型別嗎?PHP物件型別
- JavaScript中的Object的引用型別JavaScriptObject型別