web-dev-qa-db-ja.com

範囲ループ内で選択したキーをマップから削除しても安全ですか?

選択したキーをマップから削除するにはどうすればよいですか?以下のコードのように、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

101
Everton

これは安全です!同様のサンプルは Effective Go にもあります。

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

そして 言語仕様

マップの反復順序は指定されておらず、反復ごとに同じになる保証はありません。まだ到達していないマップエントリが反復中に削除された場合、対応する反復値は生成されません。マップエントリが反復中に作成された場合、そのエントリは反復中に生成されるか、スキップされる場合があります。選択は、作成されるエントリごとに、および反復ごとに異なる場合があります。マップがnilの場合、反復回数は0です。

139
Sebastian

セバスチャンの答えは正確ですが、私はなぜそれが安全であるかを知りたかったので、 Map source codedelete(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値がチェックされます。スキップできるかどうかを確認します。

要約すると、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
_
135
Verran

メモリリークが発生する可能性があるかどうか疑問に思っていました。そこで、テストプログラムを作成しました。

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
vitvlkv

要するに、はい。以前の回答を参照してください。

そしてこれも here から:

ianlancetaylorは2015年2月18日にコメントしました
これを理解するための鍵は、for/rangeステートメントの本体の実行中に、現在の反復がないことを認識することだと思います。表示されている値のセットと、表示されていない値のセットがあります。本体の実行中に、表示されたキー/値のペアの1つ(最新のペア)が範囲ステートメントの変数に割り当てられました。そのキー/値のペアについて特別なことは何もありません。これは、反復中にすでに見られたものの1つにすぎません。

彼が答えている質問は、range操作中に所定の位置にあるマップ要素を変更することです。それが彼が「現在の反復」に言及している理由です。ただし、ここでも関連性があります。範囲内でキーを削除できます。これは、後で範囲内でそれらのキーが表示されないことを意味します(すでに見た場合は大丈夫です)。

0
Larry Clapp