¿Es seguro eliminar las claves seleccionadas del mapa dentro de un bucle de rango?

135

¿Cómo se pueden eliminar las claves seleccionadas de un mapa? ¿Es seguro combinarlo delete()con el rango, como en el siguiente código?

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
fuente

Respuestas:

174

Esto es seguro! También puede encontrar una muestra similar en Effective Go :

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

Y la especificación del lenguaje :

El orden de iteración sobre los mapas no se especifica y no se garantiza que sea el mismo de una iteración a la siguiente. Si las entradas del mapa que aún no se han alcanzado se eliminan durante la iteración , no se generarán los valores de iteración correspondientes. Si las entradas del mapa se crean durante la iteración , esa entrada se puede generar durante la iteración o se puede omitir. La elección puede variar para cada entrada creada y de una iteración a la siguiente. Si el mapa es nulo, el número de iteraciones es 0.

Sebastian
fuente
key.expired undefined (la cadena de tipo no tiene campo ni método caducado)
44
@kristen: en el ejemplo descrito anteriormente, la clave no debe ser una cadena, sino un tipo personalizado que implemente la func (a T) expired() boolinterfaz. Para los propósitos de este ejemplo, puede intentar: 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
Muy confuso.
g10guang
150

La respuesta de Sebastian es precisa, pero quería saber por qué era segura, así que investigé el código fuente del mapa . Parece que en una llamada a delete(k, v), básicamente solo establece un indicador (además de cambiar el valor de conteo) en lugar de eliminar el valor:

b->tophash[i] = Empty;

(Vacío es una constante para el valor 0)

Lo que el mapa parece estar haciendo realmente es asignar un número establecido de cubetas dependiendo del tamaño del mapa, que crece a medida que realiza inserciones a la velocidad de 2^B(a partir de este código fuente ):

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

Por lo tanto, casi siempre hay más depósitos asignados de los que está utilizando, y cuando hace un rangesobre el mapa, verifica el tophashvalor de cada depósito 2^Bpara ver si puede omitirlo.

Para resumir, deletedentro de a rangees seguro porque los datos técnicamente todavía están allí, pero cuando lo comprueba tophash, ve que puede omitirlo y no incluirlo en cualquier rangeoperación que esté realizando. El código fuente incluso incluye un TODO:

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

Esto explica por qué el uso de la delete(k,v)función en realidad no libera memoria, solo la elimina de la lista de cubos a los que tiene acceso. Si desea liberar la memoria real, deberá hacer que todo el mapa sea inalcanzable para que intervenga la recolección de basura. Puede hacerlo usando una línea como

map = nil
Verran
fuente
2
Entonces, parece que estás diciendo que es seguro eliminar cualquier valor arbitrario del mapa, no solo el 'actual', ¿correcto? ¿Y cuando llegue el momento de evaluar un hash que he eliminado previamente de forma arbitraria, lo omitirá de forma segura?
Flimzy
@Flimzy Eso es correcto, como puedes ver en este patio de juegos play.golang.org/p/FwbsghzrsO . Tenga en cuenta que si el índice que elimina es el primero en el rango, seguirá mostrándolo ya que ya está escrito en k, v, pero si establece el índice en cualquiera que no sea el primero que el rango encuentra, solo mostrará dos teclas / pares de valores en lugar de tres y no cunda el pánico.
Verran
1
¿Sigue siendo relevante el "en realidad no libera memoria"? Traté de encontrar en la fuente ese comentario, pero no puedo encontrarlo.
Tony
11
Nota importante: recuerde que esta es solo la implementación actual , y puede cambiar en el futuro, por lo que no debe confiar en ninguna propiedad adicional que pueda parecer "compatible". Las únicas garantías que tiene son las que proporciona la especificación, como se describe en la respuesta de Sebastian . (Dicho esto, explorar y explicar el funcionamiento interno Go es seguro interesante, educando y en general increíble!)
akavel
4

Me preguntaba si podría ocurrir una pérdida de memoria. Entonces escribí un programa de prueba:

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 GC libera la memoria. Entonces está bien.

vitvlkv
fuente
0

En resumen, si. Ver respuestas anteriores.

Y también esto, desde aquí :

ianlancetaylor comentó el 18 de febrero de 2015.
Creo que la clave para comprender esto es darse cuenta de que al ejecutar el cuerpo de una declaración for / range, no hay una iteración actual. Hay un conjunto de valores que se han visto y un conjunto de valores que no se han visto. Al ejecutar el cuerpo, uno de los pares clave / valor que se ha visto, el par más reciente, se asignó a la (s) variable (s) de la declaración de rango. No hay nada especial en ese par clave / valor, es solo uno de los que ya se ha visto durante la iteración.

La pregunta que está respondiendo es sobre la modificación de los elementos del mapa en su lugar durante una rangeoperación, por lo que menciona la "iteración actual". Pero también es relevante aquí: puede eliminar claves durante un rango, y eso solo significa que no las verá más adelante en el rango (y si ya las vio, está bien).

Larry Clapp
fuente