Безопасно ли удалять выбранные ключи с карты в пределах цикла диапазона?

136

Как удалить выбранные ключи с карты? Безопасно ли комбинировать 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

Эвертон
источник

Ответы:

175

Это безопасно! Вы также можете найти аналогичный образец в Effective Go :

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

И спецификация языка :

Порядок итераций по картам не указан и не гарантируется, что он будет одинаковым от одной итерации к другой. Если записи карты, которые еще не были достигнуты, удалены во время итерации , соответствующие значения итерации не будут созданы. Если записи карты создаются во время итерации , эта запись может быть создана во время итерации или может быть пропущена. Выбор может варьироваться для каждой созданной записи и от одной итерации к другой. Если карта равна нулю, количество итераций равно 0.

Себастьян
источник
key.expired undefined (строка типа не имеет поля или метод истек)
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

Ответ Себастьяна точен, но я хотел знать, почему это безопасно, поэтому я немного покопался в исходном коде карты . Похоже, что при вызове 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чтобы увидеть, может ли он пропустить его.

Подводя итог, можно сказать, что deleteвнутри a 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
Verran
источник
2
Похоже, вы говорите, что безопасно удалить любое произвольное значение с карты, а не только «текущее», правильно? И когда придет время оценить хэш, который я ранее произвольно удалил, он безопасно его пропустит?
Flimzy
@Flimzy Это верно, как вы можете видеть на этой игровой площадке play.golang.org/p/FwbsghzrsO . Обратите внимание, что если удаляемый вами индекс является первым в диапазоне, он все равно будет отображать этот индекс, поскольку он уже записан в k, v, но если вы установите любой индекс, кроме первого, который найдет диапазон, он будет отображать только два ключа пары / значение вместо трех и не паникуйте.
Verran
1
Актуален ли вопрос «фактически не освобождает память»? Я попытался найти в источнике этот комментарий, но не нашел.
Тони
11
Важное примечание: помните, что это только текущая реализация , и она может измениться в будущем, поэтому вы не должны полагаться на какие-либо дополнительные свойства, которые она может «поддерживать». В Единственные гарантии у вас есть это те , которые предусмотрены в спецификации, как описано в ответ Себастьяна . (Тем не менее, изучение и объяснение внутреннего устройства 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)
}

Похоже, сборщик мусора освобождает память. Так что все в порядке.

vitvlkv
источник
0

Короче да. См. Предыдущие ответы.

А также отсюда :

ianlancetaylor прокомментировал 18 февраля 2015 г.
Я думаю, что ключом к пониманию этого является осознание того, что при выполнении тела оператора for / range текущей итерации нет. Есть набор значений, которые были просмотрены, и набор значений, которые не были просмотрены. При выполнении тела одна из наблюдаемых пар ключ / значение - самая последняя пара - была назначена переменной (ам) оператора диапазона. В этой паре ключ / значение нет ничего особенного, это просто одна из тех, которые уже были замечены во время итерации.

Вопрос, на который он отвечает, касается изменения элементов карты на месте во время rangeоперации, поэтому он упоминает «текущую итерацию». Но это также актуально здесь: вы можете удалять ключи во время диапазона, а это просто означает, что вы не увидите их позже в диапазоне (и если вы уже видели их, это нормально).

Ларри Клэпп
источник