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

Eve*_*ton 115 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

https://play.golang.org/p/u1vufvEjSw

Seb*_*ian 154

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

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}
Run Code Online (Sandbox Code Playgroud)

语言规范:

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

  • @kristen - 在上面描述的例子中,键不应该是字符串,而是一些实现`func(a T)expired()bool`接口的自定义类型.出于这个例子的目的,你可以尝试:`m:= make(map [int] int)``/*在这里以某种方式填充m*/``for key:= range(m){``if key%2 = = 0 {/*这只是一些条件,比如调用expired*/``delete(m,key);``}``}` (3认同)

Ver*_*ran 140

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

b->tophash[i] = Empty;
Run Code Online (Sandbox Code Playgroud)

(Empty是值的常量0)

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

byte    *buckets;     // array of 2^B Buckets. may be nil if count==0.
Run Code Online (Sandbox Code Playgroud)

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

总而言之,deletea range中的数据是安全的,因为数据在技术上仍然存在,但是当它检查时tophash它看到它可以跳过它并且不包括在range你正在执行的任何操作中.源代码甚至包括TODO:

 // TODO: consolidate buckets if they are mostly empty
 // can only consolidate if there are no live iterators at this size.
Run Code Online (Sandbox Code Playgroud)

这解释了为什么使用该delete(k,v)函数实际上不会释放内存,只需将其从您允许访问的存储区列表中删除即可.如果你想释放实际内存,你需要让整个地图无法访问,这样垃圾收集就会介入.你可以使用像

map = nil
Run Code Online (Sandbox Code Playgroud)

  • **重要说明:**请记住,这只是*当前实现*,并且将来可能会发生变化,因此您不得依赖它可能看起来"支持"的任何其他属性.***只保证你有规范提供的那些,[如Sebastian的回答所述](http://stackoverflow.com/a/23230406/98528).***(那就是说,探索和解释Go内部确实很有趣,教育和一般很棒!) (10认同)
  • “实际上并没有释放内存”仍然相关吗?我试图在来源中找到该评论,但找不到。 (3认同)
  • 所以听起来你是说从地图中删除任意值是安全的,而不仅仅是“当前”,对吗?当需要评估我之前任意删除的哈希时,它会安全地跳过它吗? (2认同)

vit*_*lkv 7

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

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)
}
Run Code Online (Sandbox Code Playgroud)

看起来 GC 确实释放了内存。所以没关系。


Lar*_*app 6

简而言之,是的。请参阅之前的答案。

还有这个,从这里开始

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

他回答的问题是关于在操作期间修改地图元素range,这就是他提到“当前迭代”的原因。但这也与此相关:您可以在范围内删除键,这仅意味着您稍后不会在范围内看到它们(如果您已经看到它们,那也没关系)。