Volver al blog

La palabra clave map en Go: maps nil, orden de iteración y concurrencia

Cómo funciona la palabra clave map en Go: literales frente a make, el panic del map nil, comma-ok, orden de iteración aleatorio, claves comparables, acceso concurrente, sets y el paquete maps.

La palabra clave map en Go: maps nil, orden de iteración y concurrencia

La palabra clave map declara la tabla hash integrada de Go. El tipo map[K]V asocia claves de tipo K con valores de tipo V, con búsqueda, inserción y borrado en tiempo constante de media. Los maps forman parte del lenguaje, así que no importas ningún paquete ni instancias una clase HashMap. Tienes una sintaxis literal, las funciones integradas make, delete, clear y len, y soporte para for range (especificación de Go).

Resumen rápido

  • map[string]int{"/healthz": 1} o make(map[string]int) crea un map. var m map[string]int te da un map nil.
  • Leer de un map nil devuelve el valor cero. Escribir en uno provoca un panic con assignment to entry in nil map.
  • Una clave que no existe devuelve el valor cero. Usa v, ok := m[k] cuando necesites saber si la clave estaba.
  • El orden de iteración no está especificado y es aleatorio. Ordena las claves con slices.Sorted(maps.Keys(m)) cuando el orden importe.
  • Las claves tienen que ser comparables. Los strings, los números, los structs y los arrays sirven. Los slices, los maps y las funciones no.
  • Un valor map es un descriptor pequeño. Pasarlo a una función permite que la función modifique el map de quien la llama.
  • m[k].Field = x no compila cuando el valor es un struct. Cópialo, modifícalo y vuelve a guardarlo, o guarda punteros.
  • Los maps no son seguros para escrituras concurrentes. Protégelos con un sync.Mutex o usa sync.Map en los casos para los que se diseñó.
  • map[string]struct{} es el set de Go. El paquete maps (Go 1.21+) añade Clone, Equal, DeleteFunc, Keys y Collect.

¿Cómo se declara y se crea un map en Go?

Un tipo map se escribe map[KeyType]ValueType. Creas un map con un literal compuesto cuando ya conoces algunas entradas, o con make cuando empiezas vacío:

example.gogo
package main

import "fmt"

func main() {
	statusText := map[int]string{
		200: "OK",
		404: "Not Found",
		503: "Service Unavailable",
	}

	hits := make(map[string]int)
	hits["/api/orders"]++
	hits["/api/orders"]++
	hits["/healthz"]++

	fmt.Println(statusText[404], len(statusText))
	fmt.Println(hits["/api/orders"], hits["/healthz"], len(hits))
}
example.texttext
Not Found 3
2 1 2

hits["/api/orders"]++ funciona con una clave que todavía no existe. La lectura devuelve 0, el incremento lo convierte en 1 y la escritura lo guarda. Un contador no necesita ninguna preparación.

make acepta una pista de tamaño opcional, como en make(map[string]int, len(requests)). El runtime reserva espacio para más o menos esa cantidad de entradas, así que el map no tiene que crecer mientras lo llenas. La pista no es un límite. El map sigue creciendo por encima de ella. len(m) devuelve el número de entradas, y cap no funciona con maps.

var frente a make en Go compara las dos formas de obtener un map vacío, y explica por qué var por sí solo no basta para un map.

¿Por qué escribir en un map nil provoca un panic?

El valor cero de un tipo map es nil. Un map nil no tiene ninguna tabla hash detrás, así que no hay dónde poner una entrada nueva. Esto suele aparecer como un campo map dentro de un struct que nadie inicializó:

example.gogo
package main

import "fmt"

type Router struct {
	routes map[string]string
}

func main() {
	var r Router
	fmt.Println(r.routes["/health"] == "", len(r.routes))
	r.routes["/health"] = "healthHandler"
}
example.texttext
true 0
panic: assignment to entry in nil map

Leer funciona. Un map nil se comporta como un map vacío para búsquedas, len, range y delete, así que la primera línea se imprime con normalidad. Solo la escritura provoca el panic. La solución es crear el map antes de la primera escritura, normalmente en un constructor:

example.gogo
func NewRouter() *Router {
	return &Router{routes: make(map[string]string)}
}

Un decodificador como json.Unmarshal reserva el map por ti cuando encuentra un objeto JSON, así que un destino map[string]any puede empezar siendo nil. Tu propio código tiene que llamar a make o usar un literal.

¿Cómo se comprueba si una clave existe en un map de Go?

Indexar un map con una clave que no existe devuelve el valor cero del tipo de valor. Para un contador es justo lo que quieres, porque una ruta que nadie ha pedido tiene 0 visitas. Para otros valores, el valor cero oculta una diferencia real. Un map de feature flags devuelve false para un flag desactivado y también para un flag que nunca se definió.

La forma comma-ok devuelve un segundo booleano que indica si la clave estaba presente:

example.gogo
package main

import "fmt"

func main() {
	flags := map[string]bool{
		"new_checkout": true,
		"dark_mode":    false,
	}

	for _, name := range []string{"new_checkout", "dark_mode", "beta_search"} {
		enabled, ok := flags[name]
		if !ok {
			fmt.Printf("%s: unknown flag\n", name)
			continue
		}
		fmt.Printf("%s: %v\n", name, enabled)
	}
}
example.texttext
new_checkout: true
dark_mode: false
beta_search: unknown flag

ok solo vale false para beta_search. Sin él, una errata en el nombre de un flag desactivaría la funcionalidad sin avisar. Usa la forma simple cuando el valor cero sea una respuesta correcta, como en recuentos y sumas. Usa comma-ok cuando «no existe» signifique algo distinto de «cero».

¿Cómo se borran claves y se vacía un map?

delete(m, k) elimina una clave. Borrar una clave que no está no hace nada y no es un error. clear(m), añadido en Go 1.21, elimina todas las entradas y mantiene el map reservado (notas de la versión Go 1.21):

example.gogo
cache := map[int]string{42: "[email protected]", 7: "[email protected]"}

delete(cache, 42)
delete(cache, 999)
fmt.Println(len(cache), cache)

clear(cache)
fmt.Println(len(cache), cache == nil)
example.texttext
1 map[7:[email protected]]
0 false

Después de clear, el map está vacío pero sigue siendo utilizable, y todas las variables que apuntan a él ven el map vacío. Si en cambio asignas cache = map[int]string{}, solo esa variable recibe un map nuevo.

Se permite borrar claves mientras recorres el mismo map con range. La especificación dice que una entrada eliminada durante la iteración no aparecerá más adelante en el bucle (especificación de Go). También se permite añadir claves durante la iteración, pero una entrada nueva puede aparecer o no en ese mismo bucle.

¿Por qué el orden de iteración de un map es aleatorio en Go?

La especificación dice que el orden de iteración sobre un map no está especificado y que no se garantiza que sea el mismo de una iteración a la siguiente. El runtime va más allá y elige un punto de partida aleatorio en cada range. Tres ejecuciones del mismo bucle sobre hits imprimieron:

example.texttext
/api/orders 41 /healthz 120 /api/users 17
/api/orders 41 /healthz 120 /api/users 17
/api/users 17 /api/orders 41 /healthz 120

La aleatorización es deliberada. Las primeras versiones de Go tenían un orden que parecía estable en maps pequeños, y los programas empezaron a depender de él. Cuando cambió la implementación, esos programas dejaron de funcionar. Aleatorizar el orden hace que el bug aparezca en tus tests y no en producción (blog de Go, Go maps in action).

Cuando necesites un orden estable, para una línea de log, un informe o un test, ordena las claves. Desde Go 1.23, maps.Keys devuelve un iterador y slices.Sorted lo recoge y lo ordena en una sola llamada:

example.gogo
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	hits := map[string]int{"/api/orders": 41, "/healthz": 120, "/api/users": 17}

	for _, path := range slices.Sorted(maps.Keys(hits)) {
		fmt.Printf("%-12s %d\n", path, hits[path])
	}
}
example.texttext
/api/orders  41
/api/users   17
/healthz     120

fmt.Println de un map completo ya imprime las claves ordenadas, y por eso la salida de fmt.Println(cache) de más arriba es estable. encoding/json también ordena las claves del map al serializarlo. Solo range es aleatorio.

¿Qué tipos pueden ser claves de un map en Go?

Un tipo de clave tiene que ser comparable, es decir, == y != tienen que estar definidos para él. Los strings, los enteros, los floats, los booleanos, los punteros, los canales y las interfaces son comparables. También lo son los structs y los arrays cuyos campos o elementos son todos comparables. Los slices, los maps y las funciones no lo son, y el compilador los rechaza:

example.gogo
seen := map[[]string]bool{}
example.texttext
./main.go:4:14: invalid map key type []string

Las claves struct son útiles cuando la clave tiene más de una parte. Aquí la latencia de cada endpoint usa como clave el método y la ruta juntos:

example.gogo
type Endpoint struct {
	Method string
	Path   string
}

latencyMs := map[Endpoint]int{
	{"GET", "/api/orders"}:  38,
	{"POST", "/api/orders"}: 112,
}
fmt.Println(latencyMs[Endpoint{"POST", "/api/orders"}])

Esto imprime 112. El struct se compara campo a campo, así que dos valores Endpoint con el mismo método y la misma ruta encuentran la misma entrada. Así te ahorras el viejo truco de construir un string de clave como "POST /api/orders" y volver a partirlo después. Para usar una lista de IDs como clave, convierte el slice en un array de tamaño fijo o únelo en un string.

Las claves de tipo interfaz como map[any]int compilan, pero insertar un slice en tiempo de ejecución provoca un panic con runtime error: hash of unhashable type []string. Las claves float funcionan, pero son mala idea. NaN no es igual a sí mismo, así que cada m[math.NaN()] = 1 añade una entrada nueva que nunca podrás volver a buscar.

¿Los maps se pasan por referencia en Go?

Go lo pasa todo por valor, incluidos los maps. El valor de una variable map es un descriptor pequeño que apunta a la tabla hash, así que copiarlo copia el puntero y no las entradas. Una función que recibe un map puede cambiar las entradas de quien la llama. Una función que asigna un map nuevo a su parámetro solo cambia su propia copia:

example.gogo
func recordStatus(counts map[int]int, status int) {
	counts[status]++
}

func resetCounts(counts map[int]int) {
	counts = map[int]int{}
}

func main() {
	counts := map[int]int{}
	recordStatus(counts, 200)
	recordStatus(counts, 500)
	resetCounts(counts)
	fmt.Println(counts)
}
example.texttext
map[200:1 500:1]

recordStatus escribió a través de la tabla compartida, así que los dos recuentos están ahí. resetCounts reemplazó su variable local y no tuvo ningún efecto en quien la llamó. Para vaciar el map de quien llama, usa clear(counts) dentro de la función. go vet no lo señala, así que la segunda función compila y no hace nada sin avisar.

Lo mismo ocurre con los campos de structs y los valores de retorno. Un getter que devuelve un map interno permite que quien lo llama modifique tu estado. Devuelve maps.Clone(m) cuando quien llama deba recibir su propia copia.

¿Por qué no puedo asignar a un campo de un struct dentro de un map?

Un valor de un map no es direccionable. El runtime puede mover las entradas cuando la tabla crece, así que Go no te da una referencia a un valor guardado dentro de ella. Por eso esto no compila:

example.gogo
type Session struct {
	UserID   int
	Requests int
}

sessions := map[string]Session{"s_91": {UserID: 42}}
sessions["s_91"].Requests++
example.texttext
./main.go:10:2: cannot assign to struct field sessions["s_91"].Requests in map

Hay dos soluciones. Puedes copiar el valor, cambiarlo y volver a guardarlo, o puedes guardar punteros, de modo que el map contenga direcciones y el struct viva fuera de la tabla:

example.gogo
s := sessions["s_91"]
s.Requests++
sessions["s_91"] = s

byToken := map[string]*Session{"s_91": {UserID: 42}}
byToken["s_91"].Requests++

Las dos dejan Requests en 1. La forma de copiar y guardar mantiene el map como único dueño de los datos. La forma con punteros es más corta de actualizar y evita copiar structs grandes, pero una clave inexistente te da un puntero nil, así que byToken["s_404"].Requests++ provoca un panic. Compruébalo antes con comma-ok. Esta misma regla es la razón por la que no puedes escribir &sessions["s_91"].

¿Son seguros los maps de Go para uso concurrente?

No. Las lecturas concurrentes no dan problemas, pero una escritura concurrente con cualquier otra lectura o escritura es una data race. El runtime detecta algunas de estas races y detiene el programa entero con un error fatal que recover no puede capturar. Ocho goroutines que incrementan un map de contadores compartido fallan enseguida:

example.gogo
hits := map[string]int{}
var wg sync.WaitGroup
for range 8 {
	wg.Go(func() {
		for range 10000 {
			hits["/api/orders"]++
		}
	})
}
wg.Wait()
example.texttext
fatal error: concurrent map writes

Una lectura que compite con una escritura da fatal error: concurrent map read and map write. En un servidor HTTP esto ocurre en cuanto dos peticiones tocan un map compartido, porque cada petición se ejecuta en su propia goroutine. Ejecuta tus tests con go test -race para detectar las races que la comprobación del runtime no ve.

La solución habitual es poner el map y un sync.Mutex en un mismo tipo y acceder al map solo a través de métodos que bloquean:

example.gogo
type HitCounter struct {
	mu   sync.Mutex
	hits map[string]int
}

func NewHitCounter() *HitCounter {
	return &HitCounter{hits: make(map[string]int)}
}

func (c *HitCounter) Inc(path string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.hits[path]++
}

func (c *HitCounter) Get(path string) int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.hits[path]
}

Las mismas ocho goroutines ahora terminan y c.Get("/api/orders") devuelve 80000. Usa sync.RWMutex con RLock en Get cuando las lecturas superen con mucho a las escrituras y cada lectura haga un trabajo real.

sync.Map es un map concurrente de la biblioteca estándar. Su documentación dice que está optimizado para dos casos: claves que se escriben una vez y se leen muchas, como una caché que solo crece, y goroutines que trabajan sobre conjuntos de claves disjuntos (documentación de sync.Map). Fuera de esos casos, un map normal con un mutex suele ser más sencillo e igual de rápido, y conserva tus tipos de clave y valor en lugar de any.

¿Cómo se hace un set en Go?

Go no tiene un tipo set. La forma idiomática es un map con un struct vacío como valor. struct{} ocupa cero bytes, así que el map solo guarda claves. Esto elimina peticiones duplicadas según su ID:

example.gogo
requestIDs := []string{"req_7f", "req_a1", "req_7f", "req_c3", "req_a1"}

seen := make(map[string]struct{}, len(requestIDs))
for _, id := range requestIDs {
	if _, dup := seen[id]; dup {
		fmt.Println("duplicate, skipping", id)
		continue
	}
	seen[id] = struct{}{}
}
fmt.Println(len(seen), "unique requests")
example.texttext
duplicate, skipping req_7f
duplicate, skipping req_a1
3 unique requests

map[string]bool también funciona y se lee un poco mejor, porque if seen[id] no necesita comma-ok. Cuesta un byte por entrada y permite un tercer estado, una clave guardada con false. Elige una y úsala en todo el proyecto. La palabra clave struct en Go trata struct{} y su otro uso como señal en canales.

¿Qué hace el paquete maps?

El paquete maps llegó en Go 1.21 con funciones genéricas, y Go 1.23 le añadió funciones de iteradores (pkg.go.dev/maps). Estas son las que más aparecen:

example.gogo
defaults := map[string]string{"region": "eu-west-1", "log_level": "info"}

cfg := maps.Clone(defaults)
cfg["log_level"] = "debug"
cfg["debug_token"] = "tok_123"

fmt.Println(maps.Equal(cfg, defaults))

maps.DeleteFunc(cfg, func(k, v string) bool {
	return strings.HasPrefix(k, "debug_")
})
fmt.Println(slices.Sorted(maps.Keys(cfg)))
fmt.Println(defaults["log_level"])

roles := maps.Collect(slices.All([]string{"admin", "editor", "viewer"}))
fmt.Println(roles[1])
example.texttext
false
[log_level region]
info
editor
  • maps.Clone hace una copia superficial. Cambiar cfg no tocó defaults. Los valores en sí no se copian en profundidad, así que un map de slices sigue compartiendo los slices.
  • maps.Equal compara claves y valores con ==. No puedes comparar dos maps directamente con ==, porque los maps solo se pueden comparar con nil.
  • maps.DeleteFunc elimina todas las entradas que cumplen un predicado.
  • maps.Keys, maps.Values y maps.All devuelven iteradores. Pásalos a slices.Sorted, a slices.Collect o a un bucle for range.
  • maps.Collect construye un map a partir de cualquier iterador de clave y valor. Aquí convierte un slice en un map de índice a valor. maps.Copy(dst, src) fusiona un map dentro de otro.

¿Cómo están implementados los maps de Go?

Desde Go 1.24, el map integrado usa un diseño Swiss Table, una tabla hash de direccionamiento abierto que examina grupos de posiciones a la vez. Las notas de la versión indican un menor coste de CPU en las operaciones con maps (notas de la versión Go 1.24). El lenguaje no cambió, así que el código escrito para los antiguos maps basados en buckets funciona igual que antes.

Los maps siguen sin encogerse nunca. Borrar entradas libera las posiciones para reutilizarlas, pero conserva la memoria. Una caché que llegó a tener un millón de sesiones conserva esa memoria después de borrarlas. clear tampoco la libera. Si un map que vive mucho tiempo crece de golpe y luego se vacía, copia las entradas que quedan en un map nuevo con maps.Clone o con un make nuevo, y deja que el recolector libere el antiguo.

Dónde encaja LevelUpGo

LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Composite Types trata los maps junto a los slices y los structs, incluidas las búsquedas con comma-ok, el recuento, el borrado y la iteración en un orden estable. Concurrency Fundamentals trata las goroutines, los WaitGroups y los mutex que necesitas en cuanto un map se comparte entre ellas. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.

Preguntas frecuentes

¿Es map una palabra clave en Go?

Sí. map es una de las 25 palabras clave reservadas de Go, así que no puedes usar map como nombre de variable ni de función. Aparece en tipos map como map[string]int, en literales de map y como argumento de make.

¿Un map de Go mantiene el orden?

No. La especificación deja sin especificar el orden de iteración, y el runtime lo aleatoriza en cada range. Para procesar las claves en orden, ordénalas con slices.Sorted(maps.Keys(m)). fmt.Println y json.Marshal ya imprimen las claves del map ordenadas.

¿Son thread-safe los maps de Go?

No. Las lecturas concurrentes son seguras, pero cualquier escritura junto a otra lectura o escritura puede tumbar el programa con fatal error: concurrent map writes. Protege el map con un sync.Mutex o un sync.RWMutex, o usa sync.Map para cachés que se escriben una sola vez y para conjuntos de claves disjuntos.

¿Cómo se obtiene la longitud de un map en Go?

Llama a len(m). Devuelve el número de entradas y funciona con un map nil, en cuyo caso devuelve 0. Los maps no admiten cap.

¿Tiene Go un tipo set?

No. Usa map[T]struct{}, que solo guarda claves, y comprueba la pertenencia con _, ok := set[k]. map[T]bool también funciona si prefieres if set[k] en lugar de comma-ok.

¿Qué pasa cuando lees una clave que no existe?

Obtienes el valor cero del tipo de valor, como 0, "", false o nil, y ningún error. Usa la forma con dos valores v, ok := m[k] cuando necesites distinguir una clave inexistente de un valor cero guardado.

Fuentes

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita