在 Go 中删除 map 的键:底层隐藏了什么?

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

Delete key in map:它在 Go 中是如何在底层工作的

\n\n

您是否曾想过,当您在 Go 中写下 delete(myMap, "key") 时,实际发生了什么?键只是从内存中消失了吗?还是留下了一个“空洞”?如果从 nil map 中删除键会怎样?如果您曾问过自己这些问题——您并不孤单。关于 delete key in map 的话题在 Stack Overflow 上已有数千次浏览,今天我们将深入探讨这个看似简单的操作背后的机制。

\n\n

在这篇 SEO 文章中,我们将剖析 Go 中哈希表(hmap 结构)的构成、键的删除是如何工作的、内存的去向,以及为什么不必担心在循环中使用 delete。开始吧!

\n\n

简要回顾:Go 中的 map 是如何构成的

\n\n

在讨论删除之前,让我们先回顾一下 map golang 是如何存储数据的。其内部是一个哈希表,即 hmap 结构。它由一个“桶”(buckets)数组组成,每个桶最多可以存储 8 个键值对。当您添加一个键时,Go 会计算哈希值,找到对应的桶,并将键值对放入其中。如果桶溢出,则会创建一个溢出桶(overflow bucket)链。

\n\n

这是一个经典的实现,但有一些我们将在下面讨论的技巧。

\n\n

hmap 结构(简化版)

\n\n
type hmap struct {\n    count     int     // 元素数量\n    flags     uint8\n    B         uint8   // 桶数量的对数\n    buckets   unsafe.Pointer // 指向桶数组的指针\n    oldbuckets unsafe.Pointer // 在 map 扩容时使用\n    nevacuate uintptr\n    // ...\n}
\n\n

关键点:buckets 是一个连续数组。删除操作不会压缩这个数组,而只是将单元格标记为“空”。

\n\n

Delete key in map:它在代码层面是如何工作的

\n\n

在 Go 中调用 delete(m, k) 是一个内置函数,它会被编译为运行时调用。其内部大致执行以下步骤:

\n\n
    \n
  1. 计算键的哈希值——Go 使用特定于键类型的哈希函数。
  2. \n
  3. 查找桶——根据哈希值的低位确定桶的编号。
  4. \n
  5. 线性搜索桶内部(以及溢出链)——找到包含目标键的单元格。
  6. \n
  7. 清理内存——Go 将键和值的内存清零,以便垃圾回收器可以回收它们。
  8. \n
  9. 将单元格标记为“空”——使用特殊的标志 emptyRest 或 emptyOne。
  10. \n
  11. 减少计数器——m.count--。
  12. \n
\n\n

所有这些操作平均耗时 O(1),但考虑到冲突,最坏情况下可能为 O(n)。

\n\n

代码示例:delete 的实际操作

\n\n
package main\n\nimport "fmt"\n\nfunc main() {\n    m := map[string]int{\n        "apple":  5,\n        "banana": 3,\n        "cherry": 7,\n    }\n\n    fmt.Println("删除前:", m)\n    delete(m, "banana")\n    fmt.Println("删除后:", m)\n\n    // 尝试删除一个不存在的键\n    delete(m, "grape") // 什么也不会发生,没有错误\n    fmt.Println("删除不存在的键后:", m)\n}
\n\n

请注意:对不存在的键执行 delete key in map 是一个安全操作。它只是什么都不做。这是新手常见的误解之一。

\n\n

内存发生了什么?神话与现实

\n\n

许多开发者担心在 delete 之后,map 中会留下“空洞”,从而导致内存泄漏。让我们来弄清楚。

\n\n

键和值的内存

\n\n

当您删除一个键时,Go 会将该元素的内存清零。如果键或值是指针或包含指针(字符串、切片、map),那么垃圾回收器可以回收它们。桶中的单元格本身仍然存在,但被标记为空。

\n\n

这意味着 map 的大小不会缩小。

博客

书籍推荐