É seguro remover chaves selecionadas do mapa dentro de um loop de intervalo?

135

Como remover as chaves selecionadas de um mapa? É seguro combinar delete()com o alcance, como no código abaixo?

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

Everton
fonte

Respostas:

174

Isso é seguro! Você também pode encontrar uma amostra semelhante no Effective Go :

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

E a especificação da linguagem :

A ordem de iteração nos mapas não é especificada e não é garantida a mesma de uma iteração para a seguinte. Se as entradas do mapa que ainda não foram alcançadas forem removidas durante a iteração , os valores correspondentes da iteração não serão produzidos. Se as entradas do mapa forem criadas durante a iteração , essa entrada poderá ser produzida durante a iteração ou poderá ser ignorada. A escolha pode variar para cada entrada criada e de uma iteração para a próxima. Se o mapa for nulo, o número de iterações é 0.

Sebastian
fonte
key.expired indefinido (tipo string não possui campo ou método expirado) #
4
@ kristen - no exemplo descrito acima, a chave não deve ser uma string, mas algum tipo personalizado que implementa a func (a T) expired() boolinterface. Para os fins deste exemplo, você pode tentar: 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
Muito confuso.
G10guang
150

A resposta de Sebastian é precisa, mas eu queria saber por que era seguro, então eu procurei no código-fonte do Mapa . Parece que em uma chamada para delete(k, v), basicamente, apenas define um sinalizador (além de alterar o valor da contagem) em vez de realmente excluir o valor:

b->tophash[i] = Empty;

(Vazio é uma constante para o valor 0)

O que o mapa parece realmente estar fazendo é alocar um número definido de buckets, dependendo do tamanho do mapa, que cresce à medida que você executa inserções na taxa de 2^B(a partir deste código-fonte ):

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

Portanto, quase sempre há mais buckets alocados do que você está usando, e quando você faz um rangeover-the-map, ele verifica o tophashvalor de cada bucket 2^Bpara ver se ele pode ignorá-lo.

Para resumir, o deletedentro de um rangeé seguro porque os dados ainda estão tecnicamente disponíveis, mas quando verifica, tophashele vê que pode simplesmente ignorá-lo e não incluí-lo em qualquer rangeoperação que você esteja executando. O código fonte ainda inclui um TODO:

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

Isso explica por que o uso da delete(k,v)função não libera memória, apenas a remove da lista de buckets que você tem permissão para acessar. Se você quiser liberar a memória real, precisará tornar o mapa inteiro inacessível para que a coleta de lixo entre. Você pode fazer isso usando uma linha como

map = nil
Verran
fonte
2
Parece que você está dizendo que é seguro excluir qualquer valor arbitrário do mapa, não apenas o valor 'atual', correto? E quando chegar a hora de avaliar um hash que eu excluí arbitrariamente anteriormente, ele será ignorado com segurança?
Flimzy
@Flimzy Está correto, como você pode ver neste playground play.golang.org/p/FwbsghzrsO . Observe que, se o índice que você excluir for o primeiro no intervalo, ele ainda mostrará que um já foi gravado em k, v, mas se você definir o índice para qualquer um além do primeiro que o intervalo encontrar, exibirá apenas duas teclas / value pares em vez de três e não entre em pânico.
Verran
1
O "realmente não libera memória" ainda é relevante? Tentei encontrar na fonte esse comentário, mas não consigo encontrá-lo.
Tony
11
Nota importante: lembre-se de que esta é apenas a implementação atual e pode ser alterada no futuro; portanto, você não deve confiar em nenhuma propriedade adicional que possa parecer "dar suporte". As únicas garantias que você tem são as fornecidas pela especificação, conforme descrito na resposta de Sebastian . (Dito isto, explorando e explicando os internos ir é certeza interessante, educar e geralmente incrível!)
akavel
4

Fiquei me perguntando se um vazamento de memória poderia acontecer. Então eu escrevi um programa de teste:

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)
}

Parece que o GC libera a memória. Então está tudo bem.

vitvlkv
fonte
0

Em suma, sim. Veja as respostas anteriores.

E também isso, daqui :

ianlancetaylor comentado em 18 de fevereiro de 2015
Acho que a chave para entender isso é perceber que, ao executar o corpo de uma instrução for / range, não há iteração atual. Há um conjunto de valores que foram vistos e um conjunto de valores que não foram vistos. Durante a execução do corpo, um dos pares de chave / valor que foi visto - o par mais recente - foi atribuído às variáveis ​​da instrução range. Não há nada de especial nesse par de chave / valor, é apenas um dos que já foram vistos durante a iteração.

A pergunta que ele está respondendo é sobre a modificação dos elementos do mapa durante uma rangeoperação, e é por isso que ele menciona a "iteração atual". Mas também é relevante aqui: você pode excluir as chaves durante um intervalo, e isso significa apenas que você não as verá mais tarde (e se você já as viu, tudo bem).

Larry Clapp
fonte