【问题标题】: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] = &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

【问题讨论】:

    标签: dictionary for-loop go


    【解决方案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 操作期间修改地图元素,这就是他提到“当前迭代”的原因。但这在这里也很重要:您可以在一个范围内删除键,这意味着您稍后将不会在该范围内看到它们(如果您已经看到它们,那没关系)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-06-29
        • 2014-10-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-05-08
        • 1970-01-01
        相关资源
        最近更新 更多