在范围循环内从地图中删除选定的键是不是安全?
Posted
技术标签:
【中文标题】在范围循环内从地图中删除选定的键是不是安全?【英文标题】:Is it safe to remove selected keys from map within a range loop?在范围循环内从地图中删除选定的键是否安全? 【发布时间】:2014-06-07 10:34:40 【问题描述】:如何从地图中删除选定的键?
将delete()
与范围结合起来是否安全,如下面的代码所示?
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] = &Infostr
for key, value := range table
fmt.Printf("deleting %v=>%v\n", key, value.value)
delete(table, key)
https://play.golang.org/p/u1vufvEjSw
【问题讨论】:
【参考方案1】:这是安全的!您还可以在Effective Go 中找到类似的示例:
for key := range m
if key.expired()
delete(m, key)
还有the language specification:
未指定映射的迭代顺序,也不保证从一次迭代到下一次迭代顺序相同。如果在迭代期间删除了尚未到达的映射条目,则不会生成相应的迭代值。如果地图条目在迭代期间创建,则该条目可能在迭代期间产生或可能被跳过。对于创建的每个条目以及从一个迭代到下一个迭代,选择可能会有所不同。如果map为nil,则迭代次数为0。
【讨论】:
key.expired undefined(类型字符串没有过期的字段或方法) @kristen -- 在上面描述的示例中,键不应该是字符串,而是实现func (a T) expired() bool
接口的一些自定义类型。对于此示例,您可以尝试:m := make(map[int]int)
/* populate m here somehow */
for key := range (m)
if key % 2 == 0 /* this is just some condition, such as calling expired */
delete(m, key);
非常混乱。【参考方案2】:
Sebastian 的回答是准确的,但我想知道为什么它是安全的,所以我对Map source code 进行了一些挖掘。看起来像是在调用delete(k, v)
,它基本上只是设置一个标志(以及更改计数值)而不是实际删除该值:
b->tophash[i] = Empty;
(空是值0
的常量)
地图看起来实际上正在做的是根据地图的大小分配一组桶,当您以2^B
(来自this source code)的速率执行插入时,它会增长:
byte *buckets; // array of 2^B Buckets. may be nil if count==0.
因此,分配的存储桶几乎总是比您使用的多,当您在地图上执行range
时,它会检查该2^B
中每个存储桶的tophash
值以查看它是否可以跳过在它上面。
总而言之,range
中的 delete
是安全的,因为从技术上讲,数据仍然存在,但是当它检查 tophash
时,它发现它可以跳过它而不将其包含在任何 @987654336 中@您正在执行的操作。源代码甚至包括一个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
【讨论】:
所以听起来你说从地图中删除任意值是安全的,而不仅仅是“当前”值,对吗?当需要评估我之前任意删除的哈希时,它会安全地跳过它吗? @Flimzy 没错,正如您从这个操场上看到的那样 play.golang.org/p/FwbsghzrsO 。请注意,如果您删除的索引是该范围内的第一个索引,它仍然会显示该索引,因为它已经写入 k,v 但是如果您将索引设置为除范围找到的第一个索引之外的任何索引,它只会显示两个键/value 对而不是三个而不是恐慌。 “实际上并没有释放内存”仍然相关吗?我试图在源中找到该评论但找不到它。 重要提示:请记住,这只是当前实现,未来可能会发生变化,因此您不能依赖任何其他属性它可能看起来“支持”。 只有规范提供的保证,as described in Sebastian's answer。(也就是说,探索和解释 Go 的内部机制确实很有趣,很有教育意义,而且通常很棒!) 【参考方案3】:我想知道是否会发生内存泄漏。于是我写了一个测试程序:
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 确实释放了内存。所以没关系。
【讨论】:
【参考方案4】:简而言之,是的。查看以前的答案。
还有这个,来自here:
ianlancetaylor 于 2015 年 2 月 18 日发表评论 我认为理解这一点的关键是要意识到在执行 for/range 语句的主体时,没有当前的迭代。有一组看过的值,也有一组没看过的值。在执行主体时,已看到的键/值对之一(最近的对)被分配给范围语句的变量。该键/值对没有什么特别之处,它只是在迭代过程中已经看到的其中之一。
他回答的问题是关于在range
操作期间修改地图元素,这就是他提到“当前迭代”的原因。但这在这里也很重要:您可以在一个范围内删除键,这意味着您稍后将不会在该范围内看到它们(如果您已经看到它们,那没关系)。
【讨论】:
以上是关于在范围循环内从地图中删除选定的键是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章
如何检查选定区域的纬度/经度是不是在使用 Java 的数据库中定义的范围内
检查是不是有一种方法可以在任意范围 F 内从 S 点到达 G 点