在范围循环中从 map 中删除选定的键是否安全?

如何从地图中删除选定的键? 如下面的代码所示,将 delete()与 range 组合在一起是否安全?

package main


import "fmt"


type Info struct {
value string
}


func main() {
table := make(map[string]*Info)


for i := 0; i < 10; i++ {
str := fmt.Sprintf("%v", i)
table[str] = &Info{str}
}


for key, value := range table {
fmt.Printf("deleting %v=>%v\n", key, value.value)
delete(table, key)
}
}

Https://play.golang.org/p/u1vufvejsw

67115 次浏览

这是安全的! 你也可以在 现在开始中找到类似的样本:

for key := range m {
if key.expired() {
delete(m, key)
}
}

还有 语言规范:

没有指定映射上的迭代顺序,也不能保证从一个迭代到下一个迭代是相同的。如果尚未到达的映射条目是 在迭代过程中删除,则不会产生相应的迭代值。如果映射条目是 在迭代过程中创建,那么该条目可以在迭代过程中生成,也可以跳过。对于每个创建的条目以及从一个迭代到下一个迭代,选择可能会有所不同。如果映射为零,则迭代次数为0。

塞巴斯蒂安的回答是准确的,但我想知道 为什么它是安全的,所以我做了一些挖掘的 地图源代码。它看起来像是对 delete(k, v)的调用,它基本上只是设置一个标志(以及更改 count 值) ,而不是实际删除该值:

b->tophash[i] = Empty;

(Empty 是值 0的常数)

映射实际上似乎在做的是根据映射的大小分配一组数量的存储桶,当您以 2^B(从 这个源代码开始)的速度执行插入时,存储桶会增加:

byte    *buckets;     // array of 2^B Buckets. may be nil if count==0.

所以几乎总是有比你使用更多的桶分配,当你在地图上做一个 range,它检查 2^B中每个桶的 tophash值,看看它是否可以跳过它。

总而言之,range中的 delete是安全的,因为数据在技术上仍然存在,但是当它检查 tophash时,它看到它可以跳过它,而不包括它在你正在执行的任何 range操作中。源代码甚至包括一个 TODO:

 // TODO: consolidate buckets if they are mostly empty
// can only consolidate if there are no live iterators at this size.

这解释了为什么使用 delete(k,v)函数实际上并不释放内存,只是将其从允许访问的存储桶列表中删除。如果您想释放实际的内存,您需要使整个映射不可访问,这样垃圾收集就会介入。可以使用类似于

map = nil

我想知道是否会发生内存泄漏,所以我编写了一个测试程序:

package main


import (
log "github.com/Sirupsen/logrus"
"os/signal"
"os"
"math/rand"
"time"
)


func main() {
log.Info("=== START ===")
defer func() { log.Info("=== DONE ===") }()


go func() {
m := make(map[string]string)
for {
k := GenerateRandStr(1024)
m[k] = GenerateRandStr(1024*1024)


for k2, _ := range m {
delete(m, k2)
break
}
}
}()


osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt)
for {
select {
case <-osSignals:
log.Info("Recieved ^C command. Exit")
return
}
}
}


func GenerateRandStr(n int) string {
rand.Seed(time.Now().UnixNano())
const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
}
return string(b)
}

看来 GC 确实释放了内存,所以没关系。

简而言之,是的。参见前面的答案。

还有这个,来自 给你:

Ianlancetaylor 于2015年2月18日发表评论
我认为理解这一点的关键是要认识到,在执行 for/range 语句的主体时,不存在当前的迭代。有一组已经看到的值和一组尚未看到的值。在执行主体时,已经看到的键/值对之一——最近的一对——被分配给 range 语句的变量。这个键/值对没有什么特别的,它只是在迭代过程中已经看到的其中一个。

他回答的问题是关于在 range操作期间修改映射元素,这就是他提到“当前迭代”的原因。但这里也是相关的: 您可以在一个范围内删除键,这只是意味着您将不会在以后的范围内看到它们(如果您已经看到了它们,那也没关系)。