Map in Go
Contents
TL;DR
map(散列表)是无序的键值对集合。
一个 map 中的元素不是变量,不能取 map 元素的地址,其中一个原因是逐渐增长的 map 会自动扩容,键值可能重新散列到新的存储空间,使得原先的地址无效。
遍历 map 的顺序是不确定的,不同的实现可能使用不同的散列函数从而得到不同的顺序。
- 这种设计是有意为之的,意在防止程序依赖这种不确定的顺序,使程序在不同实现的 map 面前更健壮。
map 类型的零值是 nil
,表示没有指向任何散列表。取值、delete
、len
、range
操作都可以用于 nil
map,但往 nil
map 存储值会 panic,存储之前必须分配空间。
map 之间不能直接相互比较,唯一合法的比较是和 nil
的比较;比较两个 map 的键和对应的值是否相同,必须手写循环。
map
map(散列表)是无序的键值对集合。键是唯一的,无论散列表有多大,对某一个键的取值、更新、删除操作所需要的键的比较次数一般来说是常数。
Go 中的 map 是对散列表的 <strong>引用</strong> 。
map 类型的写法是 map[K][V]
,K
是键的类型,V
是键的值。
- 一个 map 中键的类型相同,值的类型相同,键和值的类型不需要相同。
- The key type
K
must be comparable using==
, so that the map can test whether a given key is equal to one already within it.- 尽管浮点数可以比较大小,但由于存在精度问题,尤其当存在值可能是
NaN
的情况时,用浮点数作为键是不好的选择。
- 尽管浮点数可以比较大小,但由于存在精度问题,尤其当存在值可能是
map 对值的类型没有限制,可以是复合类型,如 map、切片。
创建 map:
ages := make(map[string]int)
ages["a"] = 1
ages["b"] = 2
delete(ages, "a")
// map literals
ages := map[string]int{
"a": 1,
"b": 2,
}
// a new empty map
m := map[string]int{}
访问 map 元素用 []
,删除用 delete
。
- 这些操作即使元素不在 map 中也可以安全执行;访问不存在的键返回的是键类型的零值。
+=
和 ++
也可用在 map 元素上。
一个 map 中的元素不是变量,不能取 map 元素的地址,其中一个原因是逐渐增长的 map 可能被重新散列到新的存储空间,使得原先的地址无效。
可以用 for k, v := range ages {}
遍历 map 中的键值对。遍历 map 的顺序是不确定的,不同的实现可能使用不同的散列函数从而得到不同的顺序。
- 这种设计是有意为之的,意在防止程序依赖这种不确定的顺序,使程序在不同实现的 map 面前更健壮。
若需要按一定的顺序遍历 map,则必须先对键进行排序:
names := make([]string, 0, len(ages))
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
map 类型的零值是 nil
,表示没有指向任何散列表。取值、delete
、len
、range
操作都可以用于 nil
map,但往 nil
map 存储值会 panic,存储之前必须分配空间。
var ages map[string]int
ages == nil // true (only legal comparison between maps)
len(ages) == 0 // true
ages["sb"] = 29 // panic: assignment to entry in nil map
ages = make(map[string]int) // allocate first
ages["sb"] = 29 // ok
对 map 的取值操作都会返回值,有的时候需要区分某个键是否在 map 中,可以用以下写法:
if age, ok := age["x"]; !ok { /* x is not a key in this map, ok == false, age == 0 */ }
- 第二个返回值
ok
是键是否存在在 map 中的标识。
map 之间不能直接相互比较,唯一合法的比较是和 nil
的比较;比较两个 map 的键和对应的值是否相同,必须手写循环。
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
Go 中没有集合这一数据类型,利用 map 的键是唯一的这一特点,可以用 map 来实现集合。
func main() {
seen := make(map[string]bool)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
if !seen[line] {
seen[line] = true
fmt.Println(line)
}
}
if err := input.Err(); err != nil {
fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
os.Exit(1)
}
}
将切片作为的 map 的键的做法:
- 定义一个帮助函数
k
将每个切片映射到一个字符串,满足仅当s1 == s2
时k(s1) == k(s2)
。 - 创建一个键是字符串类型的 map,键的值是
k(slice)
。
var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }
- 同样的方法也可以推广到其它不可比较的数据类型,不只是切片。
- 用这种方法还可以自己定义
==
的规则,比如比较字符串类型时用大小写不敏感的方式。
惰性填充一个 map 的惯用模式是当某个键第一次出现时,为该键的值做初始化。
var graph = make(map[string]map[string]bool)
func addEdge(from, to string) {
edges := graph[from] // 读取某个键的值
if edges == nil { // 键不存在时取到的是对应类型的零值
edges = make(map[string]bool)
graph[from] = edges
}
edges[to] = true
}
func hasEdge(from, to string) bool {
return graph[from][to]
}
实现原理
Go 使用哈希表作为 map 的底层实现。
hmap
是 map 类型的 header(map 的描述符),存储了后续 map 操作所需的所有信息。
// https://github.com/golang/go/blob/go1.12/src/runtime/map.go#L114
type hmap struct {
count int // # live cells == size of map. Must be first (used by len() builtin); // map 中的元素个数
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items); // 2^B == bucket 的数量
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details // overflow bucket 的大致数量
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. // 指向 bucket 数组的指针
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing // map 扩容阶段指向前一个 bucket 数组的指针
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) // map 扩容阶段充当扩容进度计数器,所有下标号小于 nevacuate 的 bucket 都已经完成了数据排空和迁移操作
extra *mapextra // optional fields // 如果有 overflow bucket 存在且 key、value 都因不包含指针而被内联(inline)的情况,该字段将存储所有指向 overflow bucket 的指针,保证 overflow bucket 始终可用(不被垃圾回收掉)
}
// https://github.com/golang/go/blob/go1.12/src/runtime/map.go#L63
// Maximum number of key/elem pairs a bucket can hold.
const (
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits
)
// https://github.com/golang/go/blob/go1.12/src/cmd/compile/internal/gc/reflect.go#L53
const (
BUCKETSIZE = 8
MAXKEYSIZE = 128
MAXVALSIZE = 128
)
- 存储键值对数据的是
buckets
数组(数组大小是2^B
),每个 bucket(桶)能存储的键值对个数为BUCKETSIZE
。
每个 bucket 由三部分组成:
-
tophash
区域;- 当向 map 插入一条数据或从 map 按 key 查询数据的时候,运行时会使用哈希函数对 key 做哈希运算并获得一个哈希值 hashcode。运行时将 hashcode 一分为二来看待,低位区的值用于选定 bucket,高位区(top byte)的值存储在
tophash
中,用于在某个 bucket 中确定 key 的位置。tophash
用于快速定位 key 的位置,避免逐个 key 比较这种代价较高的操作,空间换时间。
// https://github.com/golang/go/blob/go1.12/src/runtime/map.go#L148 type bmap struct { // 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. tophash [bucketCnt]uint8 // Followed by bucketCnt keys and then bucketCnt elems. // NOTE: packing all the keys together and then all the elems together makes the // code a bit more complicated than alternating key/elem/key/elem/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8. // Followed by an overflow pointer. }
- 当超过 8 个键映射到同一个 bucket 时且 map 尚未达到扩容条件时,会创建新的 bucket,
overflow
指针存储了新 bucket 的地址,形成链式结构。overflow
未在结构体中显示声明,运行时访问 bucket 时通过指针的偏移来访问虚拟成员。
- 当向 map 插入一条数据或从 map 按 key 查询数据的时候,运行时会使用哈希函数对 key 做哈希运算并获得一个哈希值 hashcode。运行时将 hashcode 一分为二来看待,低位区的值用于选定 bucket,高位区(top byte)的值存储在
-
key 存储区域;
-
tophash
区域下面的一块连续的内存区域,存储所有的 key 数据。 -
运行时在分配 bucket 时需要知道 key 的大小。当声明一个 map 类型变量时,运行时会为该变量对应的特定 map 类型生成一个
runtime.maptype
实例(如已存在则直接复用),该实例包含了所需的 map 类型的所有元信息。// https://github.com/golang/go/blob/go1.12/src/runtime/type.go#L363 type maptype struct { typ _type key *_type elem *_type bucket *_type // internal type representing a hash bucket keysize uint8 // size of key slot elemsize uint8 // size of elem slot bucketsize uint16 // size of bucket flags uint32 } // https://github.com/golang/go/blob/go1.12/src/runtime/alg.go#L39 type typeAlg struct { // function for hashing objects of this type // (ptr to object, seed) -> hash hash func(unsafe.Pointer, uintptr) uintptr // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool }
- 编译器会将语法层面的 map 操作重写成运行时对应的函数调用,这些运行时函数的第一个参数都是
maptype
指针类型。 - 运行时利用
maptype
参数中的信息确定 key 的类型和大小,map 所用的哈希函数也存放在maptype.key.alg.hash(key, hmap.hash0)
中。 maptype
的存在让 Go 中所有 map 类型共享一套运行时 map 操作函数,无须像 C++ 那样为每种 map 类型创建一套 map 操作函数,减少最终二进制文件空间的占用。
- 编译器会将语法层面的 map 操作重写成运行时对应的函数调用,这些运行时函数的第一个参数都是
-
-
value 存储区域。
-
key 存储区域下方的一块连续的内存区域,存储 key 对应的 value。
-
Go 运行时 key 和 value 分开存储而不是一个 kv 接着一个 kv 存储,这样可以减少内存对齐带来的内存浪费。
-
如果 key 或 value 的数据长度大于一定数值,运行时不会在 bucket 中直接存储数据,而是会存储 key 或 value 数据的指针。
// https://github.com/golang/go/blob/go1.12/src/runtime/map.go#L73a const ( // Maximum key or value size to keep inline (instead of mallocing per element). // Must fit in a uint8. // Fast versions cannot handle big values - the cutoff size for // fast versions in cmd/compile/internal/gc/walk.go must be at most this value. maxKeySize = 128 maxValueSize = 128 )
-
增删改查
查:
- 计算 key 的哈希;
- 取哈希值的低位对
hmap.B
取模确定 bucket 的位置; - 取哈希值的高位,在
tophash
中查询;- 若
tophash[i]
存储的值与高位区的值相等,再找到第i
个 key 的值进行比较确认(因为可能存在冲突); - 若当前 bucket 没有找到,则依次从
overflow
指向的 bucket 中查找。 - 若当前 map 处于迁移过程中,优先从
oldbuckets
数组中查找,不再从新的buckets
数组中查找。
- 若
增、改:
- 查找 key,已存在则更新,不存在则插入。
- 若当前 map 处于迁移过程中,新元素会直接添加到新的
buckets
数组,但插入之前查找 key 是否已存在的过程仍从oldbuckets
数组开始。
删:
- 查找 key,若存在则执行删除。
负载与扩容
负载因子用于衡量哈希表的冲突情况:$$负载因子\ =\ 键数量\ /\ bucket\ 数量$$
- 负载因子过大,说明冲突严重,存取效率低;负载因子过小,说明空间利用率低。
- 当哈希表负载因子过大时,需要申请更多的 bucket 并对所有键值重新组织使其均匀分布(rehash)。
- Go 的负载因子是 6.5。
// https://github.com/golang/go/blob/go1.12/src/runtime/map.go#L68
const (
// Maximum average load of a bucket that triggers growth is 6.5.
// Represent as loadFactorNum/loadFactorDen, to allow integer math.
loadFactorNum = 13
loadFactorDen = 2
)
扩容的时机:
// https://github.com/golang/go/blob/go1.12/src/runtime/map.go#L651
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// ...
// If we hit the max load factor or we have too many overflow buckets,
// and we're not already in the middle of growing, start growing.
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
// ...
}
// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
// tooManyOverflowBuckets reports whether noverflow buckets is too many for a map with 1<<B buckets.
// Note that most of these overflow buckets must be in sparse use;
// if use was dense, then we'd have already triggered regular map growth.
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
// If the threshold is too low, we do extraneous work.
// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.
// "too many" means (approximately) as many overflow buckets as regular buckets.
// See incrnoverflow for more details.
if B > 15 {
B = 15
}
// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
return noverflow >= uint16(1)<<(B&15)
}
- 负载因子大于 6.5。
- 运行时会建立一个两倍于现有规模的
buckets
数组,为避免一次性迁移带来的延迟,真正的排空和迁移也是在进行 assign 和 delete 操作时逐步进行。- 原
buckets
数组保存在hmap.oldbuckets
指针下面,直到所有数据都迁移到新数组,它才会被释放。 - 每次访问 map 触发一次迁移,每次迁移 2 个键值对。
- 原
- 运行时会建立一个两倍于现有规模的
- overflow 的 bucket 数量大于
2^15
。- 运行时会新建一个和现有规模一样的 bucket 数组(等量扩容),然后在进行 assign 和 delete 操作时进行排空和迁移,在 bucket 间重新分配键值对,使键值对的分布更均匀。
- overflow 指向的 bucket 可能大部分空间是闲置的。
- 运行时会新建一个和现有规模一样的 bucket 数组(等量扩容),然后在进行 assign 和 delete 操作时进行排空和迁移,在 bucket 间重新分配键值对,使键值对的分布更均匀。
若能估计 map 的使用规模,使用 cap
参数对 map 实例进行初始化可以避免 map 频繁扩容。
make([string]int, mspCap)
充当 map 描述符角色的 hmap
实例自身是有状态的(hmap.flags
),且对状态的读写没有并发保护,对 map 实例进行并发写时运行时会 panic,允许并发读。
// https://github.com/golang/go/blob/go1.12/src/runtime/map.go#L99
// flags
iterator = 1 // there may be an iterator using buckets
oldIterator = 2 // there may be an iterator using oldbuckets
hashWriting = 4 // a goroutine is writing to the map
sameSizeGrow = 8 // the current map growth is to a new map of the same size
在编译阶段,Go 编译器会将语法层面的 map 操作重写成运行时对应的函数调用:
// https://github.com/golang/go/blob/go1.11/src/cmd/compile/internal/gc/walk.go
// https://github.com/golang/go/blob/go1.11/src/runtime/map.go
m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m)
v := m["key"] → v := runtime.mapaccess1(maptype, m, "key")
v, ok := m["key"] → v, ok := runtime.mapaccess2(maptype, m, "key")
m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") // v 是用于后续存储 value 的空间的地址
delete(m, "key") → runtime.mapdelete(maptype, m, "key")
sync.Map
sync.Map
适合读多写少的场景,原理是通过 read
和 dirty
实现读写分离。
- 读操作优先查询
read
,read
中不存在时再去查询dirty
;写操作只写入dirty
;读read
无需加锁,读写dirty
都要加锁。 misses
统计穿透read
访问到dirty
的次数,超过阈值后将dirty
提升为read
。- 删除操作通过标记来实现延迟删除。
sync.Map
为两种并发场景优化:
- key 只写一次,但会读取多次;
- Multiple goroutines read, write, and overwrite entries for disjoint sets of keys.
// https://github.com/golang/go/blob/go1.12/src/sync/map.go#L12
// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// Loads, stores, and deletes run in amortized constant time.
//
// The Map type is specialized. Most code should use a plain Go map instead,
// with separate locking or coordination, for better type safety and to make it
// easier to maintain other invariants along with the map content.
//
// The Map type is optimized for two common use cases: (1) when the entry for a given
// key is only ever written once but read many times, as in caches that only grow,
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
// sets of keys. In these two cases, use of a Map may significantly reduce lock
// contention compared to a Go map paired with a separate Mutex or RWMutex.
//
// The zero Map is empty and ready for use. A Map must not be copied after first use.
type Map struct {
mu Mutex
// read contains the portion of the map's contents that are safe for
// concurrent access (with or without mu held).
//
// The read field itself is always safe to load, but must only be stored with
// mu held.
//
// Entries stored in read may be updated concurrently without mu, but updating
// a previously-expunged entry requires that the entry be copied to the dirty
// map and unexpunged with mu held.
read atomic.Value // readOnly
// dirty contains the portion of the map's contents that require mu to be
// held. To ensure that the dirty map can be promoted to the read map quickly,
// it also includes all of the non-expunged entries in the read map.
//
// Expunged entries are not stored in the dirty map. An expunged entry in the
// clean map must be unexpunged and added to the dirty map before a new value
// can be stored to it.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a shallow copy of the clean map, omitting stale entries.
dirty map[interface{}]*entry
// misses counts the number of loads since the read map was last updated that
// needed to lock mu to determine whether the key was present.
//
// Once enough misses have occurred to cover the cost of copying the dirty
// map, the dirty map will be promoted to the read map (in the unamended
// state) and the next store to the map will make a new dirty copy.
misses int
}
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
m map[interface{}]*entry
amended bool // true if the dirty map contains some key not in m.
}
// An entry is a slot in the map corresponding to a particular key.
type entry struct {
// p points to the interface{} value stored for the entry.
//
// If p == nil, the entry has been deleted and m.dirty == nil.
//
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
// is missing from m.dirty.
//
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
// != nil, in m.dirty[key].
//
// An entry can be deleted by atomic replacement with nil: when m.dirty is
// next created, it will atomically replace nil with expunged and leave
// m.dirty[key] unset.
//
// An entry's associated value can be updated by atomic replacement, provided
// p != expunged. If p == expunged, an entry's associated value can be updated
// only after first setting m.dirty[key] = e so that lookups using the dirty
// map find the entry.
p unsafe.Pointer // *interface{}
}
// expunged is an arbitrary pointer that marks entries which have been deleted
// from the dirty map.
var expunged = unsafe.Pointer(new(interface{}))
Map.misses
记录未命中Map.read
、尝试去读Map.dirty
的次数;Map.misses >= len(Map.dirty)
时,dirty
会被提升为read
。- 插入的新
entry
首先存放在Map.dirty
中。 readOnly.amended
为true
表示dirty
中存在read
中不存在的 key。entry
存储 key 对应的值。- 原生 Map 不是并发安全的,删除某个 key 时需要加锁;用
entry
对值包装一层并对entry
添加状态可以回避这一问题,实现无锁的效果。 entry.p
有 3 种状态:p == nil
:entry
已被删除且m.dirty == nil
;p == expunged
:entry
已被删除,m.dirty != nil
且entry
不在Map.dirty
中;- 除了以上两种之外的情况,说明
entry
存在于Map.read.m[key]
或m.dirty[key]
中。
- 原生 Map 不是并发安全的,删除某个 key 时需要加锁;用
expunged
是一个随机的空接口指针,用于标识从Map.dirty
从删除的entry
。
Load
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// Avoid reporting a spurious miss if m.dirty got promoted while we were
// blocked on m.mu. (If further loads of the same key will not miss, it's
// not worth copying the dirty map for this key.)
// 加锁读取,二次确认确实是 miss
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) { // 未命中次数在阈值以下
return
}
// 未命中次数超过阈值,将 dirty 提升为 readOnly
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
Store
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
// 加锁读取,二次确认
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok { // read 里存在
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok { // read 里不存在,dirty 里存在
e.storeLocked(&value)
} else { // read 和 dirty 里都不存在
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
m.dirtyLocked()
// amended 置为 true
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
// tryStore stores a value if the entry has not been expunged.
//
// If the entry is expunged, tryStore returns false and leaves the entry
// unchanged.
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
// unexpungeLocked ensures that the entry is not marked as expunged.
//
// If the entry was previously expunged, it must be added to the dirty map
// before m.mu is unlocked.
func (e *entry) unexpungeLocked() (wasExpunged bool) {
// if e.p == expunged, e.p = nil, return true
// else, return false
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
// 分配 dirty 的空间,并将 read 的数据拷贝至 dirty
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
// 没有清除的 entry 才会迁移过去
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
// p == nil,即 entry 已被删除(m.Delete() 调用 e.delete() 时设置的状态),在这里将状态置为 expunged;
// 状态置为 expunged 的 entry 不会被迁移到新的 dirty 中,实现了延迟删除的效果
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
Delete
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
e.delete()
}
}
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return false
}
// 将 entry.p 标记为 nil,数据没有删除
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
References