Maps: criando, acessando e deletando chaves

1. Introdução aos Maps em Go

1.1. O que é um map?

Em Go, um map é uma coleção não ordenada de pares chave-valor, implementada internamente como uma tabela hash. É um tipo referência, o que significa que ao atribuir um map a outra variável ou passá-lo para uma função, ambas compartilham a mesma estrutura de dados subjacente.

package main

import "fmt"

func main() {
    // Declaração e inicialização
    capitais := map[string]string{
        "Brasil": "Brasília",
        "França": "Paris",
    }

    // Map é tipo referência
    outroMap := capitais
    outroMap["Brasil"] = "São Paulo"
    fmt.Println(capitais["Brasil"]) // "São Paulo" — mesmo map!
}

1.2. Comparação com slices e structs

Característica Map Slice Struct
Acesso por chave Sim (qualquer tipo comparável) Não (apenas índice numérico) Sim (apenas campo nomeado)
Dinâmico Sim Sim Não (tamanho fixo)
Ordem Não garantida Garantida N/A
Uso típico Dicionários, cache, contagens Listas, coleções ordenadas Registros com campos fixos

Use map quando precisar de associações rápidas entre chaves e valores. Use slice para sequências ordenadas. Use struct para agrupar campos relacionados de tipos diferentes.

1.3. Tipos de chave permitidos

Apenas tipos comparáveis (que suportam == e !=) podem ser usados como chave:

// Válidos
map[string]int        // string
map[int]string        // int
map[bool]float64      // bool
map[rune]string       // rune (alias para int32)

// Inválidos (não compilam)
map[[]byte]string     // slice não é comparável
map[map[int]int]int   // map não é comparável
map[interface{}]int   // interface{} só se o valor dinâmico for comparável

2. Criando Maps

2.1. Declaração com var e make

// Map nil — não pode receber atribuições
var m1 map[string]int
fmt.Println(m1 == nil) // true
// m1["chave"] = 1     // PANIC: assignment to entry in nil map

// Map vazio com make — pronto para uso
m2 := make(map[string]int)
m2["chave"] = 1 // OK

2.2. Uso de literal de map

// Literal completo
notas := map[string]float64{
    "Alice": 9.5,
    "Bob":   8.0,
    "Carol": 7.5,
}

// Literal vazio (equivalente a make)
vazio := map[string]int{}
vazio["um"] = 1 // OK

2.3. Map com capacidade inicial

Quando você sabe quantos elementos serão inseridos, especificar capacidade inicial melhora performance:

// Capacidade inicial de 100 — evita realocações
populacao := make(map[string]int, 100)
for i := 0; i < 100; i++ {
    populacao[fmt.Sprintf("cidade_%d", i)] = i * 1000
}

3. Acessando Valores e Verificando Existência

3.1. Acesso direto

contagem := map[string]int{"a": 1, "b": 2}
valor := contagem["a"]     // 1
zero := contagem["x"]      // 0 (zero value para int)

3.2. Acesso com dois valores (idiomático)

contagem := map[string]int{"a": 1}

if valor, ok := contagem["a"]; ok {
    fmt.Printf("Chave 'a' existe com valor %d\n", valor) // 1
}

if _, ok := contagem["x"]; !ok {
    fmt.Println("Chave 'x' não existe")
}

3.3. Diferença entre chave ausente e chave com zero value

tipo := map[string]string{"vazio": ""}

v1, ok1 := tipo["vazio"]   // v1 = "", ok1 = true (chave existe)
v2, ok2 := tipo["outro"]   // v2 = "", ok2 = false (chave não existe)

// Para distinguir, SEMPRE use a forma com ok

4. Inserindo e Atualizando Chaves

4.1. Sintaxe de atribuição

estoque := map[string]int{}

// Inserção
estoque["maçã"] = 10
estoque["banana"] = 5

// Atualização
estoque["maçã"] = 8 // agora maçã vale 8

4.2. Atualização condicional

estoque := map[string]int{"maçã": 10}

// Só atualiza se a chave existir
if _, ok := estoque["maçã"]; ok {
    estoque["maçã"] += 5
}

4.3. Cuidado com maps nil

var m map[string]int // nil

// Leitura é segura
fmt.Println(m["chave"]) // 0

// Escrita causa PANIC
// m["chave"] = 1 // panic: assignment to entry in nil map

// Sempre inicialize com make ou literal
m = make(map[string]int)
m["chave"] = 1 // OK

5. Deletando Chaves

5.1. Função delete()

alunos := map[int]string{1: "Alice", 2: "Bob", 3: "Carol"}

delete(alunos, 2)          // Remove Bob
delete(alunos, 99)         // Não faz nada (seguro)
fmt.Println(alunos)        // map[1:Alice 3:Carol]

5.2. Limpando um map inteiro

dados := map[string]int{"a": 1, "b": 2, "c": 3}

// Método 1: Reassign (mais eficiente)
dados = make(map[string]int)

// Método 2: Loop com delete (preserva capacidade)
for k := range dados {
    delete(dados, k)
}

5.3. Efeito colateral: delete não libera memória imediatamente

grande := make(map[int]int, 1000000)
// ... preenche com 1 milhão de elementos
for k := range grande {
    delete(grande, k) // map fica vazio, mas memória não é liberada
}
// O GC libera a memória apenas quando o map não for mais referenciado

6. Iterando sobre Maps

6.1. Loop for range

frutas := map[string]int{"uva": 3, "laranja": 5, "limão": 2}

for chave, valor := range frutas {
    fmt.Printf("%s: %d\n", chave, valor)
}

6.2. Ordem não determinística

// A cada execução, a ordem pode ser diferente
for i := 0; i < 3; i++ {
    fmt.Println("Execução", i)
    for k, v := range map[string]int{"a": 1, "b": 2, "c": 3} {
        fmt.Printf("  %s=%d", k, v)
    }
    fmt.Println()
}

6.3. Iterando apenas chaves ou apenas valores

m := map[string]int{"x": 10, "y": 20}

// Apenas chaves
for chave := range m {
    fmt.Println(chave)
}

// Apenas valores
for _, valor := range m {
    fmt.Println(valor)
}

7. Maps como Tipos de Dados Aninhados

7.1. Map de maps

// Criação
turmas := map[string]map[string]int{
    "1A": {"Alice": 85, "Bob": 90},
    "1B": {"Carol": 78},
}

// Acesso seguro
if notas, ok := turmas["1A"]; ok {
    if _, ok2 := notas["Alice"]; ok2 {
        fmt.Println("Alice está na turma 1A")
    }
}

7.2. Map de slices

// Inicialização cuidadosa
times := map[string][]string{
    "Futebol": {"Flamengo", "Palmeiras"},
}

// Adicionando a slice existente
times["Futebol"] = append(times["Futebol"], "São Paulo")

// Inicializando slice vazia para nova chave
times["Basquete"] = []string{}
times["Basquete"] = append(times["Basquete"], "Flamengo")

7.3. Verificando nil em maps aninhados

dados := map[string]map[string]int{}

// Verificação obrigatória antes de acessar
if interno, ok := dados["chave"]; ok {
    if valor, ok2 := interno["subchave"]; ok2 {
        fmt.Println(valor)
    }
}

// Ou com inicialização automática
if dados["chave"] == nil {
    dados["chave"] = make(map[string]int)
}
dados["chave"]["subchave"] = 42

8. Boas Práticas e Armadilhas Comuns

8.1. Concorrência: maps não são thread-safe

import "sync"

type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func (s *SafeMap) Get(key string) int {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.m[key]
}

func (s *SafeMap) Set(key string, value int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = value
}

8.2. Comparação de maps

import "reflect"

a := map[string]int{"a": 1}
b := map[string]int{"a": 1}

// Não funciona: fmt.Println(a == b) // compile error

// Método 1: reflect.DeepEqual
fmt.Println(reflect.DeepEqual(a, b)) // true

// Método 2: Loop manual
func equalMaps(m1, m2 map[string]int) bool {
    if len(m1) != len(m2) {
        return false
    }
    for k, v := range m1 {
        if v2, ok := m2[k]; !ok || v != v2 {
            return false
        }
    }
    return true
}

8.3. Performance: escolha de chave e crescimento dinâmico

// Chaves pequenas (int, string curta) são mais rápidas
// Evite structs grandes como chave

// Capacidade inicial evita realocações
m := make(map[int]int, 1000) // boa prática para maps grandes

// Para maps muito grandes, considere:
// - sync.Map para concorrência
// - Bibliotecas especializadas como go-immutable-radix

Referências