1. Por que m[k] puede "mentir educadamente"
Cuando ve por primera vez map, parece que todo es sencillo: si quiere un valor, lo toma de m[k]. Pero luego empieza la situacion tipica de la vida real: pregunta a un map "cual es el estado de la tarea "task42"?", y responde false. Y entonces ya piensa: "Vale, el estado es false, asi que la tarea no esta hecha". Luego resulta que la tarea "task42" ni siquiera existia.
map no es descortes — simplemente devuelve zero value y actua como si todo estuviera bien. Y no hay mala intencion en ello. Es un acuerdo en Go: leer de un map con una clave ausente no falla con un error, sino que devuelve el valor "cero" del tipo. ¿Comodo? Si. ¿Peligroso? Tambien si, si no distingue entre "la clave no existe" y "la clave existe, pero el valor es cero".
Vamos a ver como Go propone resolver esta ambiguedad de forma elegante y a la manera de Go.
Zero value y ambiguedad en map
Antes de coger la "idioma ok", conviene recordar que es zero value. Es el valor que obtiene una variable "por defecto" si la declara pero no le asigna nada manualmente. En Go no es basura de memoria, sino valores bien definidos.
Mire la tabla: hoy nos servira constantemente como explicacion de "por que m[k] devuelve precisamente esto":
| Tipo del valor V en map[K]V | Que devolvera m[k] si la clave no existe |
|---|---|
|
0 |
|
"" (cadena vacia) |
|
false |
|
nil |
|
nil |
Ahora la idea importante: al leer, map no tiene por que decir si la clave existia o no. Simplemente devuelve un valor de tipo V. Y si no hay clave, devuelve el zero value del tipo V.
Y aqui aparece la ambiguedad: el valor 0 o false es un valor perfectamente valido. Puede significar "realmente se guardo 0", o puede significar "la clave no existe".
2. Ok-idioma para leer de map
Para no adivinar, Go nos da una forma ampliada de leer de un map:
v, ok := m[k]
Aqui v es el valor (como antes), y ok es un valor booleano que responde a la pregunta: "existe realmente la clave k en el map?"
- ok == true significa: la clave esta presente, el valor se ha tomado del map.
- ok == false significa: la clave no existe, y v es solo zero value.
Casi siempre esta construccion se lee como una frase:
"Toma el valor v por la clave k, y dime si se consiguio (ok)".
Veamos un ejemplo corto que muestra la idea sin ruido innecesario:
package main
import "fmt"
func main() {
m := map[string]int{"a": 0}
v1, ok1 := m["a"]
v2, ok2 := m["b"]
fmt.Println(v1, ok1) // 0 true
fmt.Println(v2, ok2) // 0 false
}
Observe: tanto ahi como alli v vale 0. Pero el significado es radicalmente distinto. Esa es precisamente la razon de que exista ok.
Un pequeno dato para mas adelante: la construccion v, ok := ... — no es solo para map. Una idea parecida se usa en type assertion (comprobacion de tipo en una interfaz), por ejemplo v, ok := x.(T).
El formato mas comodo: if v, ok := m[k]; ok { ... }
Hacer dos asignaciones separadas a veces es incomodo: ha comprobado ok, ha trabajado con v, y despues v sigue "sobrando" en el ambito, aunque ya no se necesita.
Por eso a menudo se usa la declaracion corta directamente en el if:
if v, ok := m[k]; ok {
// clave presente, se puede usar v
} else {
// la clave no existe
}
Esto se lee casi como texto normal: "Si se encontro el valor para la clave, hacemos una cosa; si no, otra".
Veamos un ejemplo con un pequeno diccionario de precios:
package main
import "fmt"
func main() {
price := map[string]int{"apple": 100}
if v, ok := price["banana"]; ok {
fmt.Println("banana price:", v)
} else {
fmt.Println("no price for banana") // no price for banana
}
}
Un matiz importante: las variables v y ok, declaradas en el if, viven solo dentro de ese if/else. Fuera no existen, y eso es bueno, porque reduce la posibilidad de usar algo equivocado por accidente.
Cuando no se necesita el valor: _, ok := m[k]
A veces solo quiere comprobar si existe la clave. Por ejemplo: "existe esa tarea?", "existe ese login?", "existe ese comando?". El valor en si no le hace falta.
Entonces, en lugar de una variable, se escribe _ (guion bajo), es decir, "ignorar":
_, ok := m[k]
Ejemplo:
package main
import "fmt"
func main() {
set := map[string]bool{"go": false}
_, ok1 := set["go"]
_, ok2 := set["java"]
fmt.Println(ok1) // true
fmt.Println(ok2) // false
}
Y aqui llegamos a una practica muy frecuente: map[string]bool se usa a menudo como un "conjunto" (set), pero tiene una "trampa": el valor false puede ser tanto un valor real como el zero value cuando falta la clave. Por eso, una comprobacion como if m[k] { ... } puede significar algo muy distinto de lo que imagina.
4. Practica: TaskBox y la trampa de map[string]bool
Imaginemos que seguimos con nuestra aplicacion de consola educativa TaskBox (un gestor de tareas ficticio). De momento, sin estructuras ni "gran arquitectura": tenemos una lista de tareas en un slice y, por separado, un map que guarda el estado "hecho/no hecho".
Supongamos que es asi:
- tasks []string — lista de IDs de tareas (por ejemplo: "t1", "t2").
- done map[string]bool — estado de ejecucion por ID.
Ahora quiere el comando status <id>, que imprima el estado de la tarea. Y escribe, como lo haria un principiante de forma logica:
package main
import "fmt"
func main() {
done := map[string]bool{
"t1": true,
"t2": false,
}
fmt.Println(done["t1"]) // true
fmt.Println(done["t2"]) // false
fmt.Println(done["t3"]) // false (pero eso NO significa "no hecho")
}
Problema: "t3" no esta en el map, pero la salida es false. Y si interpreta false como "no hecho", acaba de introducir un bug en la aplicacion: empezara a comportarse como si la tarea existiera, solo que no estuviera hecha.
La correccion consiste en usar la ok-idioma:
package main
import "fmt"
func main() {
done := map[string]bool{"t1": true, "t2": false}
if v, ok := done["t3"]; ok {
fmt.Println("status:", v)
} else {
fmt.Println("no such task") // no such task
}
}
Ahora "la tarea no existe" y "la tarea existe con el valor false" son casos distintos, y su UX se vuelve honesta.
Mini-comando status en estilo read → compute → print
Construyamos un pequeno fragmento de TaskBox que lee un comando e imprime el estado. Haremos una entrada muy simplificada: el usuario introduce dos palabras — el comando y el id. Por ejemplo:
- status t2
- status t3
El codigo sera pequeno, pero ya se parece a una logica CLI real:
package main
import "fmt"
func main() {
done := map[string]bool{"t1": true, "t2": false}
var cmd string
var id string
fmt.Scan(&cmd, &id)
if cmd == "status" {
if v, ok := done[id]; ok {
fmt.Println("done:", v)
} else {
fmt.Println("unknown task") // unknown task
}
}
}
Aqui hay dos habitos "adultos" muy buenos que va formando desde los primeros dias:
Primero, no intenta "leer el estado" sin comprobar que la clave existe. Segundo, separa responsabilidades: el comando se comprueba por separado, y la lectura del estado, por separado. Si, todavia no es una arquitectura ideal, pero lo importante es que ya no confunde false con "no hay tarea".
5. Donde mas hace falta ok: int y string
Puede parecer que el problema solo existe con bool. En realidad aparece con cualquier tipo en el que zero value sea un valor normal.
Escenario A: map[string]int
Supongamos que tenemos descuentos:
package main
import "fmt"
func main() {
discount := map[string]int{"alice": 0, "bob": 10}
fmt.Println(discount["alice"]) // 0
fmt.Println(discount["carol"]) // 0 (pero carol no existe)
}
Aqui 0 puede significar "descuento del 0%" (valido), o puede significar "el usuario no existe".
La solucion es la misma:
package main
import "fmt"
func main() {
discount := map[string]int{"alice": 0, "bob": 10}
if v, ok := discount["carol"]; ok {
fmt.Println("discount:", v)
} else {
fmt.Println("no such user") // no such user
}
}
Escenario B: map[string]string
Una cadena vacia "" tambien puede ser valida. Por ejemplo, "no hay comentario" puede ser un valor intencional, o puede ser consecuencia de una clave ausente:
package main
import "fmt"
func main() {
comment := map[string]string{"t1": ""}
v, ok := comment["t1"]
fmt.Println(v, ok) // true
v, ok = comment["t2"]
fmt.Println(v, ok) // false
}
Observe: la impresion mostrara una cadena vacia (no se ve a simple vista), por eso, al depurar, a veces conviene imprimir cadenas con %q. Pero eso ya es mas bien para salida diagnostica; lo dominara con normalidad cuando use mas a menudo Printf.
6. Micro-esquema: como pensar al leer de map
Para fijarlo, dibujemos un pequeno diagrama de flujo "como leer de map si la clave puede faltar":
flowchart TD
A["Se necesita el valor por la clave k"] --> B["v, ok := m[k]"]
B --> C{ok == true?}
C -->|si| D["La clave existe: usamos v"]
C -->|no| E["La clave no existe: gestionamos la ausencia"]
Es un esquema muy practico: si ve m[k] en el codigo, preguntese: "y si la clave no existe?". Si la ausencia de clave es posible en la logica, entonces debe haber ok.
7. Otros lugares con ok en Go
En Go hay varias construcciones en las que el lenguaje devuelve un par "resultado + indicador de exito". La idea es la misma: no obligarle a atrapar excepciones, sino obligarle a procesar la situacion de forma explicita.
El pariente mas cercano de la comprobacion de map es la type assertion, donde tambien se escribe v, ok := x.(T). Eso se muestra directamente en los materiales estandar de Go: if e, ok := err.(*NotFoundError); ok { ... }.
Por que es importante entender esto ya, aunque hoy no estudiemos interfaces y errores en profundidad? Porque se acostumbra al estilo de Go: el lenguaje a menudo le dice "no estoy seguro de que vaya a salir — compruebe ok".
Y si aprende a respetar ok en map, mas adelante leera y escribira codigo Go mas maduro con mucha mas tranquilidad.
8. Errores tipicos al usar v, ok := m[k]
Error n. 1: pensar que ok significa "el valor es bueno/no nulo".
Los principiantes a veces interpretan ok como "hay algo util alli", y v como "no es cero". Pero ok significa solo una cosa: la clave esta presente. El valor, a su vez, puede ser 0, "", false, nil — y eso es completamente normal.
Error n. 2: comprobar if m[k] == false como si significara "la clave no existe" en un map[string]bool.
Esta es una de las trampas mas traicioneras. false puede significar que la clave existe y el valor es realmente false, o puede significar que la clave no existe. Si quiere "si/no", use _, ok := m[k]. Si quiere "valor true/false, pero solo si la clave existe", use v, ok := m[k] y separe las ramas.
Error n. 3: obtener el valor mediante m[k] y luego comprobar ok con una segunda lectura.
A veces se escribe asi: primero v := m[k], despues _, ok := m[k]. Eso es trabajo extra y ruido visual. Lo correcto es ir directamente: v, ok := m[k].
Error n. 4: declarar v, ok := ... fuera, y luego olvidar que v "se filtra" mas alla de la funcion.
Cuando v y ok viven demasiado, es mas facil equivocarse: usar por accidente v mas abajo, cuando ok era false. La construccion if v, ok := ...; ok { ... } precisamente ayuda a mantener las variables en un ambito corto y no convertirlas en "inquilinos eternos".
Error n. 5: no pensar que hacer si la clave no existe.
La ok-idioma no es solo "obtener ok". Se trata de responder con honestidad: que hace el programa si falta la clave? Imprime un mensaje? Lo omite? Crea un valor? Devuelve un error? En ejercicios de aprendizaje suele bastar con mostrar "unknown", pero en codigo real la ausencia de una clave forma parte del contrato de la funcion, y no se puede silenciar.
GO TO FULL VERSION