Golang

Maps: criação, iteração e boas práticas Já leu

8 min de leitura

Maps: criação, iteração e boas práticas
Se slices são a espinha dorsal das sequências em Go, maps são a espinha dorsal das associações. Um map armazena pares de chave

Se slices são a espinha dorsal das sequências em Go, maps são a espinha dorsal das associações. Um map armazena pares de chave e valor, permitindo recuperar qualquer valor em tempo constante — O(1) — a partir de sua chave. Essa característica os torna indispensáveis para contagem, agrupamento, indexação, cache e configuração.

Go implementa maps como tabelas hash. Você não precisa entender os detalhes internos para usá-los bem, mas saber que são tabelas hash explica dois comportamentos que todo programador Go precisa conhecer: a ordem de iteração é aleatória, e maps não são seguros para uso concorrente sem sincronização.


Criando maps

Com literal de map:

package main

import "fmt"

func main() {
    capitais := map[string]string{
        "Brasil":    "Brasília",
        "Argentina": "Buenos Aires",
        "Chile":     "Santiago",
        "Peru":      "Lima",
    }

    fmt.Println(capitais["Brasil"])     // Brasília
    fmt.Println(capitais["Argentina"])  // Buenos Aires
}

A sintaxe map[TipoChave]TipoValor define o tipo do map. Chaves e valores podem ser de qualquer tipo, com uma restrição importante: o tipo da chave precisa ser comparável — ou seja, deve suportar os operadores == e !=. Tipos como int, string, bool, structs com campos comparáveis e ponteiros são válidos como chaves. Slices, maps e funções não são comparáveis e não podem ser usados como chaves.

Com make:

estoque := make(map[string]int)
estoque["teclado"] = 15
estoque["mouse"] = 32
estoque["monitor"] = 7

fmt.Println(estoque) // map[monitor:7 mouse:32 teclado:15]

make cria um map vazio e pronto para uso. A diferença entre make(map[string]int) e declarar var m map[string]int é crítica: um map declarado com var mas não inicializado tem valor nil, e tentar escrever nele causa pânico em tempo de execução:

var m map[string]int
m["chave"] = 1  // PANIC: assignment to entry in nil map

Sempre inicialize maps com make ou com um literal antes de escrever neles.


Lendo valores e verificando existência

Ler uma chave inexistente em um map não causa erro — Go retorna o valor zero do tipo do valor:

estoque := map[string]int{
    "teclado": 15,
}

quantidade := estoque["mouse"]
fmt.Println(quantidade) // 0 — chave não existe, retorna zero

Isso pode ser ambíguo: o valor 0 pode significar "chave não existe" ou "o valor associado à chave é realmente zero". Para distinguir os dois casos, Go oferece a forma de dois retornos — o idioma mais importante ao trabalhar com maps:

quantidade, existe := estoque["mouse"]
if existe {
    fmt.Println("Estoque de mouse:", quantidade)
} else {
    fmt.Println("Produto não encontrado no estoque")
}

A variável existe é um bool que indica se a chave estava presente no map, independentemente do valor. Esse padrão é chamado de comma ok e aparece em outros contextos do Go, como leitura de channels e asserções de tipo.

Combinando com a inicialização no if:

if qtd, ok := estoque["teclado"]; ok {
    fmt.Println("Teclado em estoque:", qtd)
}

Removendo entradas

A função embutida delete remove uma chave de um map. Se a chave não existir, a operação é silenciosamente ignorada — não há erro:

estoque := map[string]int{
    "teclado": 15,
    "mouse":   32,
}

delete(estoque, "mouse")
delete(estoque, "monitor") // chave inexistente — sem efeito

fmt.Println(estoque) // map[teclado:15]

Iterando sobre maps

A iteração usa for range. Como mencionado, a ordem é aleatória e não garantida em nenhuma versão do Go:

populacao := map[string]int{
    "São Paulo":      12_300_000,
    "Rio de Janeiro":  6_700_000,
    "Brasília":        3_000_000,
    "Salvador":        2_900_000,
}

for cidade, habitantes := range populacao {
    fmt.Printf("%-20s %d habitantes\n", cidade, habitantes)
}

Quando apenas as chaves são necessárias:

for cidade := range populacao {
    fmt.Println(cidade)
}

Iteração em ordem definida. Quando a ordem importa, extraia as chaves para um slice, ordene-o e itere pelo slice:

import "sort"

chaves := make([]string, 0, len(populacao))
for k := range populacao {
    chaves = append(chaves, k)
}
sort.Strings(chaves)

for _, k := range chaves {
    fmt.Printf("%-20s %d\n", k, populacao[k])
}

Padrões comuns com maps

Contagem de frequência. Um dos usos mais clássicos de maps é contar ocorrências:

package main

import (
    "fmt"
    "strings"
)

func main() {
    texto := "go é simples go é eficiente go é concorrente"
    palavras := strings.Fields(texto)

    frequencia := make(map[string]int)
    for _, p := range palavras {
        frequencia[p]++
    }

    for palavra, count := range frequencia {
        fmt.Printf("%-15s %d\n", palavra, count)
    }
}

O padrão frequencia[p]++ funciona porque o valor zero de int é 0 — ao incrementar uma chave inexistente, Go a inicializa com 0 e então incrementa para 1.

Agrupamento. Maps cujo valor é um slice permitem agrupar elementos:

package main

import "fmt"

func main() {
    pessoas := []struct {
        Nome       string
        Departamento string
    }{
        {"Ana", "Engenharia"},
        {"Bruno", "Marketing"},
        {"Carla", "Engenharia"},
        {"Diego", "Marketing"},
        {"Elena", "Engenharia"},
    }

    porDepartamento := make(map[string][]string)
    for _, p := range pessoas {
        porDepartamento[p.Departamento] = append(
            porDepartamento[p.Departamento], p.Nome,
        )
    }

    for dept, nomes := range porDepartamento {
        fmt.Printf("%s: %v\n", dept, nomes)
    }
}

Set simulado. Go não possui um tipo Set nativo. O padrão idiomático usa map[T]struct{}, onde struct{} ocupa zero bytes de memória:

visitados := make(map[string]struct{})

urls := []string{"go.dev", "github.com", "go.dev", "pkg.go.dev"}

for _, url := range urls {
    if _, visto := visitados[url]; visto {
        fmt.Println("Duplicado ignorado:", url)
        continue
    }
    visitados[url] = struct{}{}
    fmt.Println("Processando:", url)
}

Maps de maps

Para estruturas hierárquicas, maps podem ter outros maps como valor:

config := map[string]map[string]string{
    "banco": {
        "host":  "localhost",
        "porta": "5432",
        "nome":  "appdb",
    },
    "cache": {
        "host": "localhost",
        "porta": "6379",
    },
}

fmt.Println(config["banco"]["host"])  // localhost
fmt.Println(config["cache"]["porta"]) // 6379

Ao trabalhar com maps aninhados, verifique a existência do map externo antes de acessar o interno para evitar pânico:

if db, ok := config["banco"]; ok {
    fmt.Println(db["nome"])
}

Tamanho e maps vazios

A função len retorna o número de pares chave-valor:

m := map[string]int{"a": 1, "b": 2, "c": 3}
fmt.Println(len(m)) // 3

Para verificar se um map está vazio:

if len(m) == 0 {
    fmt.Println("map vazio")
}

Concorrência e maps

Maps em Go não são seguros para uso concorrente. Se múltiplas goroutines acessam o mesmo map simultaneamente e ao menos uma delas está escrevendo, o comportamento é indefinido e pode causar corrupção de dados ou pânico.

Para uso concorrente, existem duas abordagens principais. A primeira é proteger o map com um sync.RWMutex:

import "sync"

type MapaConcorrente struct {
    mu   sync.RWMutex
    dados map[string]int
}

func (m *MapaConcorrente) Set(chave string, valor int) {
    m.mu.Lock()
    defer m.mu.Unlock()
    m.dados[chave] = valor
}

func (m *MapaConcorrente) Get(chave string) (int, bool) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    v, ok := m.dados[chave]
    return v, ok
}

A segunda é usar o sync.Map da biblioteca padrão, otimizado para casos onde chaves são escritas uma vez e lidas muitas vezes:

import "sync"

var m sync.Map

m.Store("chave", 42)

valor, ok := m.Load("chave")
if ok {
    fmt.Println(valor) // 42
}

m.Delete("chave")

O tema de concorrência será aprofundado futuramente. Por ora, a regra simples é: se mais de uma goroutine usa o mesmo map, proteja o acesso.


Resumo do que foi coberto

Este artigo cobriu maps em Go de forma completa: criação com literais e make, a distinção crucial entre map nil e map vazio, leitura com o idioma comma ok, remoção com delete, iteração ordenada, padrões de contagem, agrupamento e simulação de set, maps aninhados e as considerações de segurança para uso concorrente.


Referências e leituras complementares

Comentários

Mais em Golang

Arrays e Slices: a espinha dorsal das coleções em Go
Arrays e Slices: a espinha dorsal das coleções em Go

Toda linguagem de programação precisa de uma forma de armazenar...

Variáveis, tipos primitivos e declaração curta
Variáveis, tipos primitivos e declaração curta

  O sistema de tipos do Go é sua primeira linha de defesa! ...

Ponteiros: conceito, uso e quando evitar
Ponteiros: conceito, uso e quando evitar

Ponteiros são um dos tópicos que mais intimidam iniciantes vind...