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

huangapple go评论88阅读模式
英文:

Is it safe to remove selected keys from map within a range loop?

问题

如何从地图中删除选定的键?
在下面的代码中,将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)
    }
}

您可以使用delete()函数从地图中删除选定的键。在给定的代码中,它是安全的将delete()与范围结合使用。在循环中,它会遍历地图中的每个键值对,并使用delete()函数删除当前键。

英文:

How can one remove selected keys from a map?
Is it safe to combine delete() with range, as in the code below?

package main

import &quot;fmt&quot;

type Info struct {
	value string
}

func main() {
	table := make(map[string]*Info)
	
	for i := 0; i &lt; 10; i++ {
		str := fmt.Sprintf(&quot;%v&quot;, i)
		table[str] = &amp;Info{str}
	}

	for key, value := range table {
		fmt.Printf(&quot;deleting %v=&gt;%v\n&quot;, key, value.value)
		delete(table, key)
	}
}

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

答案1

得分: 270

这是安全的!你也可以在Effective Go中找到一个类似的示例:

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

还有语言规范

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

英文:

This is safe! You can also find a similar sample in Effective Go:

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

And the language specification:

> The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If map entries that have not yet been reached are removed during iteration, the corresponding iteration values will not be produced. If map entries are created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0.

答案2

得分: 215

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

b->tophash[i] = Empty;

(Empty是值0的常量)

实际上,地图似乎正在分配一定数量的存储桶,具体取决于地图的大小,当您执行插入操作时,它以2^B的速率增长(来自此源代码):

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

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

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

Sebastian's answer is accurate, but I wanted to know why it was safe, so I did some digging into the Map source code. It looks like on a call to delete(k, v), it basically just sets a flag (as well as changing the count value) instead of actually deleting the value:

b-&gt;tophash[i] = Empty;

(Empty is a constant for the value 0)

What the map appears to actually be doing is allocating a set number of buckets depending on the size of the map, which grows as you perform inserts at the rate of 2^B (from this source code):

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

So there are almost always more buckets allocated than you're using, and when you do a range over the map, it checks that tophash value of each bucket in that 2^B to see if it can skip over it.

To summarize, the delete within a range is safe because the data is technically still there, but when it checks the tophash it sees that it can just skip over it and not include it in whatever range operation you're performing. The source code even includes a TODO:

 // TODO: consolidate buckets if they are mostly empty
 // can only consolidate if there are no live iterators at this size.

This explains why using the delete(k,v) function doesn't actually free up memory, just removes it from the list of buckets you're allowed to access. If you want to free up the actual memory you'll need to make the entire map unreachable so that garbage collection will step in. You can do this using a line like

map = nil

答案3

得分: 10

我在想是否可能发生内存泄漏。所以我写了一个测试程序:

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)确实释放了内存,所以没问题。

英文:

I was wondering if a memory leak could happen. So I wrote a test program:

package main

import (
	log &quot;github.com/Sirupsen/logrus&quot;
	&quot;os/signal&quot;
	&quot;os&quot;
	&quot;math/rand&quot;
	&quot;time&quot;
)

func main() {
	log.Info(&quot;=== START ===&quot;)
	defer func() { log.Info(&quot;=== DONE ===&quot;) }()

	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 &lt;-osSignals:
			log.Info(&quot;Recieved ^C command. Exit&quot;)
			return
		}
	}
}

func GenerateRandStr(n int) string {
	rand.Seed(time.Now().UnixNano())
	const letterBytes = &quot;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;
	b := make([]byte, n)
	for i := range b {
		b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
	}
	return string(b)
}

Looks like GC do frees the memory. So it's okay.

答案4

得分: 5

简而言之,是的。请参考之前的回答。

还有这个来自这里的内容:

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

他回答的问题是关于在range操作期间原地修改映射元素,这就是为什么他提到了“当前迭代”。但这也与这里相关:你可以在range期间删除键,这意味着你在后面的range中将不会看到它们(如果你已经看到了它们,那没关系)。

英文:

In short, yes. See previous answers.

And also this, from here:

> ianlancetaylor commented on Feb 18, 2015
I think the key to understanding this is to realize that while executing the body of a for/range statement, there is no current iteration. There is a set of values that have been seen, and a set of values that have not been seen. While executing the body, one of the key/value pairs that has been seen--the most recent pair--was assigned to the variable(s) of the range statement. There is nothing special about that key/value pair, it's just one of the ones that has already been seen during the iteration.

The question he's answering is about modifying map elements in place during a range operation, which is why he mentions the "current iteration". But it's also relevant here: you can delete keys during a range, and that just means that you won't see them later on in the range (and if you already saw them, that's okay).

huangapple
  • 本文由 发表于 2014年4月23日 04:52:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/23229975.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定