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])
}
Desde o Go 1.23, as três etapas cabem numa linha: for _, k := range slices.Sorted(maps.Keys(populacao)), com os pacotes maps e slices da biblioteca padrão.
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
Ler de um map aninhado que não existe não causa pânico: config["cache"]["porta"], com "cache" ausente, pega o valor zero do map externo — um map interno nil — e a leitura nele devolve "". O pânico aparece na escrita: config["cache"]["porta"] = "6379" tenta gravar num map nil e para com assignment to entry in nil map. Antes de escrever, crie o map interno se ele ainda não existir:
if _, ok := config["cache"]; !ok {
config["cache"] = make(map[string]string)
}
config["cache"]["porta"] = "6379"
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 programa está errado, e o runtime costuma detectar o conflito e encerrar tudo com fatal error: concurrent map writes. Não é um pânico comum: um recover não o intercepta, e o processo morre mesmo com a proteção no lugar. Quando o runtime não percebe o conflito, o resultado são dados corrompidos em silêncio — o go test -race é a ferramenta para achar esses casos antes da produção.
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.
Map é a estrutura de busca do Go: rápida, sem ordem e com um valor zero que simplifica contagens e agrupamentos. As regras que evitam surpresas são poucas e concretas — ler de um map nil é seguro e escrever nele não é, o comma ok separa a chave ausente do valor zero, o valor guardado não pode ser alterado no lugar e, com mais de uma goroutine, o acesso precisa de sincronização, porque o erro que o runtime dispara nesse caso derruba o processo inteiro.
Fontes e leituras recomendadas
-
Go Blog: Go maps in action — Artigo oficial sobre maps, com casos de uso práticos. https://go.dev/blog/maps
-
A Tour of Go — Maps — Introdução interativa ao tipo map. https://go.dev/tour/moretypes/19
-
Go by Example: Maps — Exemplos práticos comentados. https://gobyexample.com/maps
-
Documentação do pacote sync — sync.Map — Referência do map concorrente da biblioteca padrão. https://pkg.go.dev/sync#Map
-
Especificação da linguagem Go — Map types — Definição formal do tipo map. https://go.dev/ref/spec#Map_types
-
Go by Example: Sorting — Como ordenar slices de strings e ints, útil para iteração ordenada em maps. https://gobyexample.com/sorting
Exercícios
Exercício 1
Um cadastro guarda produtos em map[string]Produto, em que Produto é uma struct com o campo Estoque. Para dar baixa, alguém escreve produtos["teclado"].Estoque-- e o código não compila. Por que o Go proíbe algo aparentemente tão natural, e quais são as duas formas de fazer a baixa?
Ver resposta
✓ Resposta: O compilador recusa com cannot assign to struct field m["a"].N in map, porque o valor guardado num map não é endereçável. A tabela hash pode mudar os elementos de lugar quando cresce, então um ponteiro para dentro dela ficaria inválido, e o Go prefere proibir a operação a permitir um ponteiro que muda de alvo por baixo dos panos. A leitura produtos["teclado"] devolve uma cópia da struct, e alterar um campo de uma cópia temporária não faria sentido. A primeira forma de dar baixa é o ciclo de copiar, alterar e gravar de volta: p := produtos["teclado"]; p.Estoque--; produtos["teclado"] = p. A segunda é guardar ponteiros, com map[string]*Produto, e aí produtos["teclado"].Estoque-- funciona, porque a alteração vai para a struct apontada, que mora fora do map. A escolha segue o critério do artigo de ponteiros: a segunda forma é mais cômoda, mas cria uma entidade compartilhada, e qualquer um que tenha o ponteiro enxerga e altera a mesma struct.
Exercício 2
Um teste imprime um map com fmt.Println e compara a saída com uma string esperada. Ele passa sempre. Um colega usa a mesma ideia num gerador de relatório e monta as linhas com for k, v := range m; o relatório sai em ordem diferente a cada execução. Por que o fmt é estável e o range não?
Ver resposta
✓ Resposta: Porque o fmt ordena as chaves antes de imprimir: map[string]int{"c": 3, "a": 1, "b": 2} sai sempre como map[a:1 b:2 c:3], comportamento adotado justamente para tornar a saída reprodutível. O range, por sua vez, não promete ordem nenhuma, e o runtime sorteia o ponto de partida a cada laço para que ninguém dependa de uma ordem acidental. O teste do primeiro colega é estável, mas por um motivo que ele talvez desconheça. O relatório precisa de ordem explícita: coletar as chaves, ordenar e percorrer a lista ordenada, o que desde o Go 1.23 se escreve como for _, k := range slices.Sorted(maps.Keys(m)). Se a ordem desejada não for a das chaves, como no caso do maior valor primeiro, ordene uma fatia de pares com slices.SortFunc. Map é para busca por chave; quando a ordem é parte do resultado, ela precisa vir de uma fatia.
Exercício 3
Um cache em memória é um map[string][]byte global, atualizado por vários handlers HTTP. Para "garantir que nada derrube o servidor", alguém põe um defer com recover em cada handler. Sob carga, o processo morre mesmo assim, com fatal error: concurrent map writes. Por que o recover não ajudou, e qual é a correção?
Ver resposta
✓ Resposta: Porque o que aconteceu não foi um pânico, e sim um erro fatal do runtime. Quando ele detecta duas goroutines escrevendo no mesmo map ao mesmo tempo, encerra o processo inteiro, e recover não tem efeito sobre isso. Num teste com oito goroutines gravando cem mil chaves cada uma num map comum, com um recover registrado, a execução termina com fatal error: concurrent map writes e o recover nunca chega a rodar. A decisão é deliberada: um map usado sem sincronização pode estar corrompido, e continuar rodando com ele seria pior que parar. A correção é sincronizar o acesso, como o artigo mostra, com um sync.RWMutex em volta do map ou com sync.Map quando o padrão for de muitas leituras e poucas escritas. Para achar o problema antes da produção, go test -race aponta a disputa já na primeira vez que ela acontece nos testes, com as duas pilhas envolvidas, em vez de depender de o runtime pegar o conflito em flagrante.
Exercício 4
Uma rotina de limpeza percorre um map de sessões com for k, v := range sessoes e chama delete(sessoes, k) para as expiradas. Um revisor bloqueia o PR: "não se remove de uma coleção enquanto se itera sobre ela". Em Go, ele tem razão?
Ver resposta
✓ Resposta: Não. Em Go, apagar entradas durante o range é permitido e está definido na especificação: uma entrada removida que ainda não tinha sido alcançada simplesmente não aparece no laço, e as outras continuam sendo percorridas normalmente. Num teste com mil sessões, apagar dentro do range as de valor par deixa exatamente 500 no map, sem pânico e sem entrada pulada. A regra que o revisor tem em mente vale para outras linguagens, como o ConcurrentModificationException do Java ou o erro que o Python levanta ao mudar o tamanho de um dicionário durante a iteração. O que o Go não garante é o efeito de acrescentar chaves durante o range: a chave nova pode ou não aparecer no mesmo laço. Remover é seguro; acrescentar deve ser feito num map separado ou depois do laço. E nada disso vale com concorrência: se outra goroutine mexe no map, é o problema do exercício anterior.
Exercício 5
Um componente guarda uma referência ao map de configuração, cfg := app.config. Na recarga, o código principal "zera" a configuração antes de preencher de novo. Com clear(app.config), o componente passa a ver o map vazio; com app.config = make(map[string]string), continua vendo os valores antigos. Explique e diga qual das duas a recarga deveria usar.
Ver resposta
✓ Resposta: Uma variável de map guarda uma referência para a tabela, e cfg := app.config faz as duas variáveis apontarem para a mesma. O clear, embutido desde o Go 1.21, esvazia essa tabela compartilhada: num teste com b := a e depois clear(a), len(b) vale 0. Já app.config = make(...) não toca na tabela antiga, só faz app.config apontar para outra, nova; com d := c seguido de c = make(...), len(c) vale 0 e len(d) continua 1. Qual usar depende do que o componente deve ver. Se ele deve acompanhar a recarga, o clear seguido do preenchimento faz isso, mas há uma janela em que ele enxerga a configuração vazia ou incompleta, e, com goroutines envolvidas, isso exige o mutex do exercício anterior. A forma mais robusta costuma ser a terceira: montar o map novo por inteiro e só então trocá-lo de uma vez, e fazer os componentes buscarem a configuração atual a cada uso em vez de guardarem uma referência própria.