go map遍历删除-面试三连击

177 阅读3分钟

参考大佬博客,下面这个结论我对后半截不是很理解。

如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。但是,遍历的结果就可能不会是相同的了,有可能结果遍历结果集中包含了删除的 key,也有可能不包含,这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。

Q1:单携程内,map可以边遍历边删除吗?

A:可以。不会panic。 因为写标记检查一直都能通过: 删除时标记1,删除后复位0,然后重复这个过程,所以可以一直正常删除。

Q2:一边遍历一边删除,遍历结果是否包含被删除的元素?

A: 分两种情况讨论

情况1:只删除当前元素

下面2种形式都是只删除当前元素,都可以遍历到全部5个元素(包括会被删除的元素),即: 遍历结果集中肯定包含了全部被删除的key

m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}

for k := range m {
    fmt.Println("Current key:", k)
    delete(m, k) // 删除当前遍历的元素
}
for k := range m {
    fmt.Println("Current key:", k)
    if k == "b" || k == "d" {
        delete(m, k) // 删除当前遍历的元素
    }
}

情况2:删除非当前遍历的元素

比如遍历顺序是a->b,如果遍历a时,把b删除了(会把b元素对应的tophash给标记为empty),之后遍历b元素发现删除标记,则会跳过该元素。 即:删除key的时间:是在遍历到 key 所在的 bucket 时刻前删除了key,结果集不会包含删除key

相反的场景,比如遍历删除是b->a,那结果会包含b,因为b先遍历到,然后在遍历a时才删除。 即:删除key的时间:是在遍历到 key 所在的 bucket 时刻后,依然会遍历到key(b),结果集会包含删除key

结论: 因为key遍历顺序随机,所以这种前后关系也会变化,所以有时结果集包含被删除元素,有时不包含被删除元素。即:有可能遍历结果集中包含了删除的 key,也有可能不包含, 这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。

m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}

for k := range m {
    fmt.Println("Current key:", k)
    if k == "a" || k == "c" || k == "e" {
        // 删除非当前遍历的元素
        if k == "a" {
            delete(m, "b")
        }
        if k == "c" {
            delete(m, "d")
        }
    }
}

fmt.Println("Final map:", m)
  • 遍历过程详细步骤

假设遍历顺序是 "a" -> "b" -> "c" -> "d" -> "e",并且我们在遍历过程中删除 "b""d",但只在访问其他键时进行删除操作。

  1. 访问 "a"

    • 输出:"Current key: a"
    • 当前 bucket0 中的元素:["a" -> 1, "b" -> 2]
    • 删除 "b"delete(m, "b")
    • 此时 bucket0 中的元素变为:["a" -> 1, emptyOne]
    • 迭代器继续前进到下一个非空元素 "c"
  2. 访问 "b"

    • 由于 "b" 已经被删除,迭代器跳过该位置。
    • 输出:无(因为 "b" 已被删除)
  3. 访问 "c"

    • 输出:"Current key: c"
    • 当前 bucket1 中的元素:["c" -> 3, "d" -> 4]
    • 删除 "d"delete(m, "d")
    • 此时 bucket1 中的元素变为:["c" -> 3, emptyOne]
    • 迭代器继续前进到下一个非空元素 "e"
  4. 访问 "d"

    • 由于 "d" 已经被删除,迭代器跳过该位置。
    • 输出:无(因为 "d" 已被删除)
  5. 访问 "e"

    • 输出:"Current key: e"
    • 当前 bucket2 中的元素:["e" -> 5]
    • 遍历结束

最终输出结果为:

Current key: a
Current key: c
Current key: e
Final map: map[a:1 c:3 e:5]

Q:遍历删除的最佳实践

先搜集删除key后再删除,不要一边遍历,一边删除

toDelete := []string{}
for k := range m {
    if someCondition(k) {
        toDelete = append(toDelete, k)
    }
}
for _, k := range toDelete {
    delete(m, k)
}