TL;DR

map(散列表)是无序的键值对集合。

一个 map 中的元素不是变量,不能取 map 元素的地址,其中一个原因是逐渐增长的 map 会自动扩容,键值可能重新散列到新的存储空间,使得原先的地址无效。

遍历 map 的顺序是不确定的,不同的实现可能使用不同的散列函数从而得到不同的顺序。

  • 这种设计是有意为之的,意在防止程序依赖这种不确定的顺序,使程序在不同实现的 map 面前更健壮。

map 类型的零值是 nil,表示没有指向任何散列表。取值、deletelenrange 操作都可以用于 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,表示没有指向任何散列表。取值、deletelenrange 操作都可以用于 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 的键的做法:

  1. 定义一个帮助函数 k 将每个切片映射到一个字符串,满足仅当 s1 == s2k(s1) == k(s2)
  2. 创建一个键是字符串类型的 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 由三部分组成:

  1. 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 时通过指针的偏移来访问虚拟成员。
  2. 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 操作函数,减少最终二进制文件空间的占用。
  3. 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
      )
      

增删改查

查:

  1. 计算 key 的哈希;
  2. 取哈希值的低位对 hmap.B 取模确定 bucket 的位置;
  3. 取哈希值的高位,在 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)
}
  1. 负载因子大于 6.5。
    • 运行时会建立一个两倍于现有规模的 buckets 数组,为避免一次性迁移带来的延迟,真正的排空和迁移也是在进行 assign 和 delete 操作时逐步进行。
      • buckets 数组保存在 hmap.oldbuckets 指针下面,直到所有数据都迁移到新数组,它才会被释放。
      • 每次访问 map 触发一次迁移,每次迁移 2 个键值对。
  2. overflow 的 bucket 数量大于 2^15
    • 运行时会新建一个和现有规模一样的 bucket 数组(等量扩容),然后在进行 assign 和 delete 操作时进行排空和迁移,在 bucket 间重新分配键值对,使键值对的分布更均匀。
      • overflow 指向的 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 适合读多写少的场景,原理是通过 readdirty 实现读写分离。

  • 读操作优先查询 readread 中不存在时再去查询 dirty;写操作只写入 dirty;读 read 无需加锁,读写 dirty 都要加锁。
  • misses 统计穿透 read 访问到 dirty 的次数,超过阈值后将 dirty 提升为 read
  • 删除操作通过标记来实现延迟删除。

sync.Map 为两种并发场景优化:

  1. key 只写一次,但会读取多次;
  2. 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.amendedtrue 表示 dirty 中存在 read 中不存在的 key。
  • entry 存储 key 对应的值。
    • 原生 Map 不是并发安全的,删除某个 key 时需要加锁;用 entry 对值包装一层并对 entry 添加状态可以回避这一问题,实现无锁的效果。
    • entry.p 有 3 种状态:
      1. p == nilentry 已被删除且 m.dirty == nil
      2. p == expungedentry 已被删除,m.dirty != nilentry 不在 Map.dirty 中;
      3. 除了以上两种之外的情况,说明 entry 存在于 Map.read.m[key]m.dirty[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