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

时间:2014-04-22 20:52:58

标签: dictionary for-loop go

如何从地图中删除选定的键? 将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

4 个答案:

答案 0 :(得分:131)

这很安全!您还可以在Effective Go中找到类似的示例:

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

the language specification

  

未指定地图上的迭代顺序,并且不保证从一次迭代到下一次迭代都是相同的。如果在迭代期间删除了尚未到达的地图条目,则不会生成相应的迭代值。如果在迭代期间创建了地图条目,则可以在迭代期间生成该条目,也可以跳过该条目。对于创建的每个条目以及从一次迭代到下一次迭代,选择可能不同。如果地图为nil,则迭代次数为0。

答案 1 :(得分:129)

塞巴斯蒂安的答案是准确的,但我想知道为什么它是安全的,所以我做了一些挖掘Map source code。看起来在调用delete(k, v)时,它基本上只是设置一个标志(以及更改计数值),而不是实际删除该值:

b->tophash[i] = Empty;

(Empty是值0的常量)

地图看起来实际上正在做的是根据地图的大小分配一定数量的存储桶,当您以2^B(来自this source code)的速率执行插入时,这会增加:

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

因此,几乎总是会分配比你正在使用的桶更多的桶,并且当你在地图上执行range时,它会检查tophash中每个桶的值2^B看看它是否可以跳过它。

总结一下,delete中的range是安全的,因为数据在技术上仍然存在,但是当它检查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

答案 2 :(得分:4)

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

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会释放内存。所以没关系。

答案 3 :(得分:0)

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

此外,来自here

  

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

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