7. Mapas#

Podemos pensar a los mapas como una generalización de los arreglos, donde en lugar de usar sólo enteros como índices, podemos usar otros tipos. Por ejemplo podemos acceder a un elemento de un mapa usando una cadena como índice edades["alice"] en lugar de un entero como edades[0].

Los mapas son una forma de asociar claves a valores, y son útiles para almacenar datos que se pueden identificar mediante una clave. Por ejemplo, en el caso de edades, la clave es el nombre de una persona y el valor es su edad.

En Go, un map es una referencia a una tabla hash[1], y el tipo de map se escribe como map[K]V, donde K y V son los tipos de sus claves y valores. Todas las claves en un mapa dado son del mismo tipo, y todos los valores son del mismo tipo, pero las claves no necesitan ser del mismo tipo que los valores.

Importante

El tipo de clave K se debe poder comparar usando ==, para que el mapa pueda verificar si una clave ya está presente o no.

No hay restricciones sobre el tipo de valor V.

Los mapas son dinámicos es decir que pueden crecer o disminuir su tamaño a medida que se agregan o eliminan elementos.

La función built-in make se puede usar para reservar la memoria que usará un mapa:

edades := make(map[string]int)

También podemos crear un mapa literal para crear un nuevo mapa con algunos pares clave/valor iniciales:

edades := map[string]int{
    "alice": 31,
    "charlie": 34,
}

Esto es equivalente a

edades := make(map[string]int)
edades["alice"] = 31
edades["charlie"] = 34

Una expresión alternativa para un nuevo mapa vacío es map[string]int{}.

Los elementos de un mapa se acceden mediante la notación habitual de subíndice:

edades["alice"] = 32
edad := edades["alice"]
fmt.Println(edad)
32

y se pueden eliminar con la función built-in delete:

delete(edades, "alice")

Todas estas operaciones son seguras incluso si el elemento no está en el mapa; una búsqueda en un mapa utilizando una clave que no está presente devuelve el valor cero para su tipo. Por ejemplo, lo siguiente funciona incluso cuando "bob" aún no es una clave en el mapa porque el valor de edades["bob"] será 0.

edades["bob"] = edades["bob"] + 1

Las formas abreviadas de asignación x += y y x++ también funcionan para los elementos de un mapa, por lo que podemos reescribir la declaración anterior como

edades["bob"] += 1

o incluso de forma más concisa como

edades["bob"]++

Para enumerar todos los pares clave/valor en el mapa, usamos un bucle for basado en range, similar a los que vimos para slices. Las iteraciones sucesivas del bucle hacen que las variables name y age se configuren con el siguiente par clave/valor:

for name, age := range edades {
    fmt.Printf("%s\t%d\n", name, age)
}
charlie  34
bob      3

Los mapas en Go no están ordenados y si mostramos todos los pares claves/valor almacenados es posible que el orden se modifique de una ejecución a la siguiente. Esto es intencional; hacer que la secuencia varíe ayuda a forzar que los programas sean robustos entre implementaciones.

Para enumerar los pares clave/valor en orden, debemos ordenar las claves explícitamente, por ejemplo, usando la función sort del paquete String:

import "sort"

var names []string

for name := range edades {
    names = append(names, name)
}

sort.Strings(names)

for _, name := range names {
    fmt.Printf("%s\t%d\n", name, edades[name])
}
bob      3
charlie  34

Dado que conocemos el tamaño final de names desde el principio, es más eficiente asignar un array con el tamaño requerido de antemano. La siguiente declaración crea un slice que inicialmente está vacío pero tiene la capacidad suficiente para contener todas las claves del mapa edades:

names := make([]string, 0, len(edades))

En el primer bucle range mencionado anteriormente, solo necesitamos las claves del mapa edades, por lo que omitimos la segunda variable del bucle. En el segundo bucle, solo necesitamos los elementos del slice names, por lo que usamos el identificador en blanco _ para ignorar la primera variable, el índice.

El valor cero para un tipo mapa es nil, es decir nulo. En otras palabras el mapa no tiene memoria asignada y no se puede usar. Un mapa nil es diferente de un mapa vacío, que es un mapa que tiene memoria asignada pero no tiene claves.

var edades map[string]int
fmt.Println(edades == nil)
fmt.Println(len(edades) == 0)
true
true

La mayoría de las operaciones sobre mapas, incluyendo la recuperación, delete, len y los bucles range, son seguras de realizar en un mapa nil, ya que se comporta como un mapa vacío. Sin embargo, almacenar en un mapa nil provoca un error:

edades["carol"] = 21
panic: assignment to entry in nil map

Antes de poder almacenar valores, se debe asignar memoria al mapa.

Acceder a un elemento de un mapa mediante subíndices siempre devuelve un valor. Si la clave está presente en el mapa, obtendrás el valor correspondiente; si no, obtendrás el valor cero para el tipo del elemento, como vimos con edades["bob"].

Para muchos propósitos, eso está bien, pero a veces necesitas saber si el elemento realmente estaba allí o no. Por ejemplo, si el tipo del elemento es numérico, podrías necesitar distinguir entre un elemento inexistente y un elemento que casualmente tiene el valor cero, utilizando una prueba como esta:

age, ok := edades["bob"]
if !ok { /* "bob" no es una clave en este mapa; age == 0. */ }

Es muy común el siguiente patrón que combina las dos sentencias anteriores dentro de la condición del if, asignación y comparación en una sola línea:

if age, ok := edades["bob"]; !ok { /* ... */ }

Utilizar un subíndice en un mapa en este contexto produce dos valores; el segundo es un booleano que indica si el elemento está presente. La variable booleana a menudo se llama ok, especialmente si se usa inmediatamente en una condición if.

Al igual que con los slices, los mapas no se pueden comparar entre sí; la única comparación legal es con nil. Para verificar si dos mapas contienen las mismas claves y los mismos valores asociados, debemos escribir un bucle.

x := map[string]int{"a": 1}
y := map[string]int{"a": 1}
fmt.Println(x == y)
invalid operation: x == y (map can only be compared to nil)

7.1. Ejercicios#

  1. Escribir una función ContarPalabras que cuente las palabras en un string y devuelva un mapa que mapee las palabras a su número de ocurrencias. La función Split del paquete strings puede ser útil.

  2. Escribir una función que compare dos mapas de cadenas y devuelva true si los mapas contienen las mismas claves y los mismos valores. Usa el siguiente prototipo: func Igual(x, y map[string]int) bool.

  3. Los anagramas son palabras que tienen las mismas letras pero en un orden diferente. Escribir una función Anagramas que tome dos strings y devuelva true si son anagramas. Usa el siguiente prototipo: func Anagramas(s1, s2 string) bool. La complejidad del algoritmo debe ser \(O(n)\), donde n es la longitud de los strings.