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


135

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

Answers:


174

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

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

语言规范

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


key.expired未定义(类型字符串没有字段或方法已过期)

4
@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); } }
abanana

非常混乱。
g10guang

150

Sebastian的答案是准确的,但我想知道为什么它是安全的,因此我对Map源代码进行了一些研究。看起来像是在调用时delete(k, v),它基本上只是设置一个标志(以及更改计数值),而不是实际删除该值:

b->tophash[i] = Empty;

(空值是一个常数0

地图似乎实际在做的是根据地图的大小分配一定数量的存储桶,当您以2^B(来自此源代码)的速率执行插入操作时,存储桶会增加:

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

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

总而言之,a之deleterange是安全的,因为技术上数据仍然存在,但是当检查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
因此,听起来您是在说可以安全地从地图中删除任意值,而不仅仅是“当前”值,对吗?当需要评估我之前任意删除的哈希时,它会安全地跳过吗?
Flimzy

@Flimzy这是正确的,正如您在操场上所看到的play.golang.org/p/FwbsghzrsO。请注意,如果删除的索引是该范围内的第一个索引,则它仍将显示该索引,因为它已经写入了k,v,但是如果将索引设置为除该范围发现的第一个索引之外的任何索引,它将仅显示两个键/值对,而不是三个,而不是恐慌。
Verran

1
“实际上并没有释放内存”是否仍然有意义?我试图在源代码中找到该评论,但找不到。
托尼

11
重要说明:请记住,这只是当前的实现,并且将来可能会更改,因此您不能依赖它可能会在“支持”中出现的任何其他属性。如Sebastian的答案中所述,您所拥有唯一保证是规范提供的保证(也就是说,探索和解释Go的内部
原理

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


0

简而言之,是的。查看以前的答案。

还有这个,从这里开始

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

他要回答的问题是关于在range操作过程中在适当位置修改地图元素的原因,这就是为什么他提到“当前迭代”。但这在这里也很重要:您可以在某个范围内删除键,这仅意味着您以后不会在该范围内看到它们(如果您已经看到它们,那没关系)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.