在范围循环内从地图中删除选定的键是不是安全?

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 点

使用 awk getline bash 在指定的时间范围内从日志文件中提取数据

如果选定的单元格在范围内,Excel 删除行

如何使用基于范围的 for 循环修改地图中的值?

有没有办法使用 tweepy 在特定时间范围内从特定用户那里获取推文?