Golang 引用型別-map

elihe2011發表於2020-10-17

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
}

相關文章