Mapas en Go
Generalmente, hay dos tipos de implementaciones de estructuras de datos de mapas, tabla hash (hash table) y árbol de búsqueda (search tree). La diferencia es que la primera es desordenada y la segunda ordenada. En Go, la implementación de map se basa en buckets hash (también un tipo de tabla hash), por lo que también es desordenado. En este artículo no se explicará en detalle el principio de implementación, ya que excede el alcance de lo básico, se analizará en profundidad más adelante.
TIP
Para entender el principio de map, puedes ir a implementación de map
Inicialización
En Go, el tipo de clave de un mapa debe ser comparable, por ejemplo string e int son comparables, mientras que []int no es comparable, por lo que no puede ser clave de un mapa. Hay dos formas de inicializar un mapa, la primera es con un literal, el formato es el siguiente:
map[tipoClave]tipoValor{}Algunos ejemplos
mp := map[int]string{
0: "a",
1: "a",
2: "a",
3: "a",
4: "a",
}
mp := map[string]int{
"a": 0,
"b": 22,
"c": 33,
}La segunda forma es usar la función incorporada make, para mapas recibe dos parámetros, el tipo y la capacidad inicial, por ejemplo:
mp := make(map[string]int, 8)
mp := make(map[string][]int, 10)El mapa es un tipo de referencia, un mapa con valor cero o sin inicializar se puede acceder, pero no se pueden almacenar elementos, por lo que se debe asignar memoria.
func main() {
var mp map[string]int
mp["a"] = 1
fmt.Println(mp)
}panic: assignment to entry in nil mapTIP
Al inicializar un mapa, se debe intentar asignar una capacidad razonable para reducir el número de expansiones.
Acceso
Acceder a un mapa es como acceder a un array mediante índices.
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp["a"])
fmt.Println(mp["b"])
fmt.Println(mp["d"])
fmt.Println(mp["f"])
}0
1
3
0A través del código se puede observar que aunque no existe el par clave-valor "f" en el mapa, todavía hay un valor de retorno. Para claves que no existen en el mapa, el valor de retorno es el valor cero del tipo correspondiente. Además, al acceder a un mapa en realidad hay dos valores de retorno, el primer valor es el valor del tipo correspondiente, el segundo valor de retorno es un booleano que indica si la clave existe, por ejemplo:
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
if val, exist := mp["f"]; exist {
fmt.Println(val)
} else {
fmt.Println("la clave no existe")
}
}Obtener la longitud de un mapa
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(len(mp))
}Almacenar valores
Almacenar valores en un mapa también es similar a almacenar valores en un array, por ejemplo:
func main() {
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
fmt.Println(mp)
}Al almacenar valores usando una clave que ya existe, se sobrescribirá el valor original
func main() {
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
if _, exist := mp["b"]; exist {
mp["b"] = 3
}
fmt.Println(mp)
}Pero también hay un caso especial, cuando la clave es math.NaN()
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
_, exist := mp[math.NaN()]
fmt.Println(exist)
fmt.Println(mp)
}false
map[NaN:c NaN:a NaN:b]A través del resultado se puede observar que el mismo valor de clave no se ha sobrescrito, sino que pueden existir múltiples, tampoco se puede determinar si existe, por lo tanto no se puede obtener el valor normalmente. Porque NaN está definido por el estándar IEE754, su implementación se completa mediante la instrucción de ensamblaje subyacente UCOMISD, que es una instrucción de comparación desordenada de números de punto flotante de doble precisión, esta instrucción considera el caso de NaN, por lo tanto el resultado es que ningún número es igual a NaN, NaN tampoco es igual a sí mismo, esto también causa que cada valor hash sea diferente. La comunidad ha discutido intensamente sobre esto, pero el equipo oficial considera que no es necesario modificarlo, por lo que se debe evitar usar NaN como clave de mapa.
Eliminar
func delete(m map[Type]Type1, key Type)Para eliminar un par clave-valor se necesita usar la función incorporada delete, por ejemplo
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp)
delete(mp, "a")
fmt.Println(mp)
}map[a:0 b:1 c:2 d:3]
map[b:1 c:2 d:3]Hay que tener en cuenta que si el valor es NaN, incluso no se puede eliminar ese par clave-valor.
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
fmt.Println(mp)
delete(mp, math.NaN())
fmt.Println(mp)
}map[NaN:c NaN:a NaN:b]
map[NaN:c NaN:a NaN:b]Recorrer
Se puede recorrer un mapa mediante for range, por ejemplo
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
for key, val := range mp {
fmt.Println(key, val)
}
}c 2
d 3
a 0
b 1Se puede ver que el resultado no está ordenado, lo que también confirma que el mapa se almacena de forma desordenada. Cabe mencionar que aunque NaN no se puede obtener normalmente, se puede acceder a través del recorrido, por ejemplo
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
for key, val := range mp {
fmt.Println(key, val)
}
}NaN a
NaN c
NaN bLimpiar
Antes de go1.21, para limpiar un mapa, solo se podía usar delete para cada clave del mapa
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
for k, _ := range m {
delete(m, k)
}
fmt.Println(m)
}Pero go1.21 actualizó la función clear, ya no es necesario hacer la operación anterior, con un solo clear se puede limpiar
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
clear(m)
fmt.Println(m)
}Salida
map[]Set
Set es una colección desordenada que no contiene elementos duplicados. Go no proporciona una estructura de datos similar, pero como las claves de un mapa son desordenadas y no se pueden repetir, también se puede usar un mapa para reemplazar un set.
func main() {
set := make(map[int]struct{}, 10)
for i := 0; i < 10; i++ {
set[rand.Intn(100)] = struct{}{}
}
fmt.Println(set)
}map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}]TIP
Una estructura vacía no ocupa memoria.
Notas
El mapa no es una estructura de datos segura para concurrencia. El equipo de Go considera que en la mayoría de los casos el uso de mapas no involucra escenarios de alta concurrencia, introducir mutex reduciría enormemente el rendimiento. El mapa tiene un mecanismo de detección de lectura y escritura interno, si hay conflictos se activará un fatal error. Por ejemplo, en los siguientes casos hay una gran probabilidad de activar un fatal.
func main() {
group.Add(10)
// map
mp := make(map[string]int, 10)
for i := 0; i < 10; i++ {
go func() {
// operación de escritura
for i := 0; i < 100; i++ {
mp["helloworld"] = 1
}
// operación de lectura
for i := 0; i < 10; i++ {
fmt.Println(mp["helloworld"])
}
group.Done()
}()
}
group.Wait()
}fatal error: concurrent map writesEn este caso, se necesita usar sync.Map para reemplazarlo.
