← Volver al listado de tecnologías

Maps

Por: Artiko
gomapsdiccionarios

Maps en Go

Un map es una coleccion desordenada de pares clave-valor. Es el equivalente a diccionarios (Python), HashMap (Java) o objetos (JavaScript).

Declaracion y Creacion

Hay dos formas principales de crear un map:

package main

import "fmt"

func main() {
    // Con make
    edades := make(map[string]int)
    edades["Ana"] = 30
    edades["Carlos"] = 25
    edades["Diana"] = 28
    fmt.Println(edades) // map[Ana:30 Carlos:25 Diana:28]

    // Literal
    capitales := map[string]string{
        "Colombia": "Bogota",
        "Peru":     "Lima",
        "Chile":    "Santiago",
    }
    fmt.Println(capitales)

    // Map vacio (no nil)
    vacio := map[string]int{}
    fmt.Println(len(vacio)) // 0
}

Comma Ok Idiom

Cuando accedes a una clave que no existe, Go retorna el zero value del tipo. Para distinguir entre “existe con zero value” y “no existe”, usa el comma ok idiom:

package main

import "fmt"

func main() {
    puntos := map[string]int{
        "Ana":    100,
        "Carlos": 0, // cero es un valor valido
    }

    // Sin comma ok: ambos retornan 0
    fmt.Println(puntos["Carlos"]) // 0
    fmt.Println(puntos["Diana"])  // 0 (no existe)

    // Con comma ok: distingue existencia
    val, ok := puntos["Carlos"]
    fmt.Printf("Carlos: val=%d existe=%t\n", val, ok) // val=0 existe=true

    val, ok = puntos["Diana"]
    fmt.Printf("Diana: val=%d existe=%t\n", val, ok) // val=0 existe=false

    // Patron comun: verificar y actuar
    if score, existe := puntos["Ana"]; existe {
        fmt.Println("Ana tiene", score, "puntos")
    } else {
        fmt.Println("Ana no encontrada")
    }
}

delete()

delete elimina una clave del map. Si la clave no existe, no hace nada (no causa panic):

package main

import "fmt"

func main() {
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
    }

    fmt.Println(m) // map[a:1 b:2 c:3]

    delete(m, "b")
    fmt.Println(m) // map[a:1 c:3]

    // Eliminar clave inexistente: no hace nada
    delete(m, "z")
    fmt.Println(m) // map[a:1 c:3]
}

Iterar con range

El orden de iteracion no esta garantizado. Cada ejecucion puede producir un orden diferente:

package main

import (
    "fmt"
    "sort"
)

func main() {
    frutas := map[string]int{
        "manzana": 5,
        "banana":  3,
        "cereza":  8,
        "durazno": 2,
    }

    // Orden no determinista
    fmt.Println("=== Orden aleatorio ===")
    for clave, valor := range frutas {
        fmt.Printf("%s: %d\n", clave, valor)
    }

    // Para orden determinista: ordenar las claves
    fmt.Println("\n=== Orden alfabetico ===")
    claves := make([]string, 0, len(frutas))
    for k := range frutas {
        claves = append(claves, k)
    }
    sort.Strings(claves)

    for _, k := range claves {
        fmt.Printf("%s: %d\n", k, frutas[k])
    }
}

Maps como Sets

Go no tiene un tipo set nativo. Se usa map[T]bool o map[T]struct{}:

package main

import "fmt"

func main() {
    // Set con bool (mas legible)
    visitados := map[string]bool{
        "Lima":   true,
        "Bogota": true,
    }

    if visitados["Lima"] {
        fmt.Println("Ya visitaste Lima")
    }

    if !visitados["Santiago"] {
        fmt.Println("No has visitado Santiago")
    }

    // Set con struct{} (no usa memoria para el valor)
    tags := map[string]struct{}{
        "go":   {},
        "rust": {},
    }

    if _, existe := tags["go"]; existe {
        fmt.Println("Tag 'go' presente")
    }

    // Agregar
    tags["python"] = struct{}{}

    // Eliminar
    delete(tags, "rust")

    fmt.Println("Tags:", len(tags))
}
EnfoqueMemoriaLegibilidadVerificacion
map[T]bool1 byte por entradaAltaif m[key]
map[T]struct{}0 bytes por valorMedia_, ok := m[key]

nil Map vs Empty Map

Un nil map permite lectura pero causa panic al escribir:

package main

import "fmt"

func main() {
    // nil map
    var m map[string]int
    fmt.Println(m == nil)  // true
    fmt.Println(len(m))    // 0
    fmt.Println(m["clave"]) // 0 (zero value, no panic)

    // m["clave"] = 1 // PANIC: assignment to entry in nil map

    // Empty map: seguro para lectura y escritura
    m2 := make(map[string]int)
    fmt.Println(m2 == nil) // false
    m2["clave"] = 1        // OK
    fmt.Println(m2)
}

Regla: siempre inicializa un map con make o literal antes de escribir en el.

Conteo de Frecuencia

Patron clasico usando maps:

package main

import (
    "fmt"
    "strings"
)

func contarPalabras(texto string) map[string]int {
    frecuencia := make(map[string]int)
    palabras := strings.Fields(texto)
    for _, p := range palabras {
        frecuencia[strings.ToLower(p)]++
    }
    return frecuencia
}

func main() {
    texto := "Go es simple Go es rapido Go es concurrente"
    freq := contarPalabras(texto)

    for palabra, conteo := range freq {
        fmt.Printf("%-12s %d\n", palabra, conteo)
    }
}

Maps No Son Concurrency-Safe

Acceder a un map desde multiples goroutines sin sincronizacion causa panic. Para uso concurrente usa sync.Map o protege con sync.Mutex:

package main

import (
    "fmt"
    "sync"
)

func main() {
    // sync.Map para uso concurrente
    var m sync.Map

    // Store (escribir)
    m.Store("clave1", 100)
    m.Store("clave2", 200)

    // Load (leer)
    if val, ok := m.Load("clave1"); ok {
        fmt.Println("clave1:", val) // 100
    }

    // LoadOrStore: retorna existente o almacena nuevo
    actual, loaded := m.LoadOrStore("clave3", 300)
    fmt.Printf("clave3: val=%v yaExistia=%t\n", actual, loaded)

    // Delete
    m.Delete("clave2")

    // Range (iterar)
    m.Range(func(key, value any) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true // continuar iterando
    })
}

sync.Map es mas eficiente que map + Mutex solo cuando las lecturas superan ampliamente a las escrituras. Para la mayoria de casos, map con sync.RWMutex es preferible.

Paquete maps (Go 1.21+)

Desde Go 1.21, el paquete maps ofrece utilidades genericas:

package main

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

func main() {
    m := map[string]int{
        "go":     1,
        "rust":   2,
        "python": 3,
    }

    // Obtener claves y valores como slices
    claves := slices.Collect(maps.Keys(m))
    valores := slices.Collect(maps.Values(m))
    fmt.Println("Claves:", claves)
    fmt.Println("Valores:", valores)

    // Clonar
    clon := maps.Clone(m)
    clon["java"] = 4
    fmt.Println("Original:", m)   // sin "java"
    fmt.Println("Clon:", clon)    // con "java"

    // Comparar
    m2 := map[string]int{"go": 1, "rust": 2, "python": 3}
    fmt.Println("Iguales:", maps.Equal(m, m2)) // true

    // Copiar entries de un map a otro
    extra := map[string]int{"zig": 5, "go": 99}
    maps.Copy(m, extra) // sobreescribe go=99, agrega zig=5
    fmt.Println("Despues de Copy:", m)

    // DeleteFunc: eliminar por condicion
    maps.DeleteFunc(m, func(k string, v int) bool {
        return v > 10
    })
    fmt.Println("Despues de DeleteFunc:", m)
}

Tipos Validos como Clave

Las claves deben ser comparables (soportar ==). Slices, maps y funciones no son validos como clave:

TipoValido como clave
int, float64, boolSi
stringSi
[N]T (array)Si (si T es comparable)
structSi (si todos los campos son comparables)
*T (puntero)Si (compara direccion)
[]T (slice)No
map[K]VNo
func()No

Ejercicios

  1. Escribe una funcion que invierta un map map[string]string (valores se convierten en claves)
  2. Implementa agrupar(personas []Persona, campo func(Persona) string) map[string][]Persona
  3. Crea una funcion que encuentre el primer caracter no repetido en un string usando un map

Resumen