Um map em Go guarda valores sob chaves e os encontra por hash. Veja por que uma chave ausente dá o zero value, por que escrever num map nil causa panic e por que a ordem de iteração muda de uma execução para outra.
Um map é o jeito de Go procurar alguma coisa pelo nome. Você dá uma chave, ele devolve o valor guardado sob essa chave, e faz isso rápido, não importa quantas entradas existam.
Maps são simples de usar e têm algumas regras que pegam as pessoas de surpresa: uma chave ausente não é um erro, um map nil pode ser lido mas não escrito, e a ordem em que as coisas voltam nunca é a ordem em que você as colocou. Este post cobre todas elas, e também o pacote maps. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.
Criando, lendo, escrevendo e apagando
Um tipo map se escreve map[K]V, em que K é o tipo da chave e V é o tipo do valor. O jeito mais rápido de criar um é com um literal:
package main
import "fmt"
func main() {
stock := map[string]int{
"apples": 5,
"bread": 2,
}
stock["cheese"] = 7 // add a key
stock["apples"] = 4 // replace a value
delete(stock, "bread")
delete(stock, "figs") // not there, so nothing happens
fmt.Println(stock["apples"], stock["cheese"], len(stock))
fmt.Println(stock)
}
Ele imprime:
4 7 2
map[apples:4 cheese:7]
m[key] = value adiciona a chave se ela é nova e substitui o valor se não é. Não existe um “insert” separado de um “update”. delete remove uma chave, e apagar uma chave que não existe não tem problema. len diz quantas chaves o map guarda.
A última linha parece ordenada, mas não tire conclusões disso. O fmt ordena as chaves de um map antes de imprimi-lo, então a saída é estável. O map em si não guarda ordem nenhuma, como uma seção mais adiante mostra.
Você também pode criar um map com make, e passar uma dica de tamanho:
package main
import (
"fmt"
"strconv"
)
func main() {
a := map[string]int{}
b := make(map[string]int)
c := make(map[string]int, 1000)
fmt.Println(len(a), len(b), len(c))
for i := range 1000 {
c[strconv.Itoa(i)] = i
}
fmt.Println(len(c), c["999"])
}
Ele imprime:
0 0 0
1000 999
a e b são a mesma coisa: um map vazio, pronto para usar. c também está vazio. O 1000 não é um tamanho. É uma dica de que vêm por aí umas 1000 entradas, para o runtime reservar espaço suficiente logo de início em vez de aumentar o map várias vezes enquanto você o preenche.
Diferente de um slice, um map não tem uma capacidade que você possa consultar. cap não aceita um map, e um map cresce sozinho conforme você adiciona chaves. Você nunca escreve m = add(m, ...) do jeito que escreve s = append(s, x).
Uma chave ausente dá o zero value
Ler uma chave que não está num map em Go não falha. Você recebe o zero value do tipo do valor, e é aí que começa a maioria dos bugs com maps:
package main
import "fmt"
func main() {
stock := map[string]int{"apples": 5, "bread": 0}
fmt.Println(stock["bread"], stock["figs"])
n, ok := stock["bread"]
fmt.Println(n, ok)
n, ok = stock["figs"]
fmt.Println(n, ok)
fmt.Println(len(stock))
}
Ele imprime:
0 0
0 true
0 false
2
stock["bread"] e stock["figs"] imprimem 0. Um quer dizer “acabou o pão”. O outro quer dizer “nunca vendemos figo”. Uma leitura simples não consegue diferenciar os dois.
A forma com dois valores consegue. n, ok := stock["figs"] coloca true em ok se a chave está no map e false se não está. Esse é o idioma comma-ok, e você o usa sempre que o zero value puder ser um valor de verdade.
A última linha também importa. Ler stock["figs"] não adicionou "figs" ao map. O tamanho continua 2. Uma leitura nunca muda um map.
Quando o zero value não pode ser uma resposta de verdade, você não precisa do ok. Um map[string]bool usado como conjunto é o caso comum: uma chave ausente é lida como false, que é exatamente a resposta que você quer.
Explicado como se você tivesse dez anos
Imagine uma parede de armários. Cada armário tem uma etiqueta na porta, como “maçãs”, e alguma coisa dentro, como o número 5. Isso é um map. A etiqueta é a chave, e o que está dentro é o valor.
Quando você pede o armário “maçãs”, recebe o que está dentro. Quando você pede um armário com a etiqueta “figos” e ele não existe, ninguém briga com você. Você recebe uma caixa vazia. Uma caixa vazia de números tem 0. Uma caixa vazia de palavras não tem nada.
Se você quer saber se o armário existia mesmo, faz uma segunda pergunta: “e esse armário existia?”. Esse é o ok.
A versão precisa
Um map não procura em todas as entradas para achar uma chave. Ele passa a chave por uma função de hash, que transforma a chave num número. Esse número escolhe uma pequena parte do map onde procurar, e só as entradas dessa parte são comparadas com a sua chave.
Aqui está a ideia, desenhada com quatro buckets. Os números dos buckets são ilustrativos, não valores de hash reais:
Uma busca passa a chave pelo hash, usa o hash para escolher um bucket e compara chaves só dentro desse bucket. Uma chave que bate retorna o valor dela e true. Uma chave que não está lá retorna o zero value e false. Os números dos buckets são ilustrativos, e a estrutura real dos maps em Go é mais elaborada que quatro buckets, mas a ideia é a mesma: o hash leva a um lugar pequeno, e só ali se procura.
Aqui estão esses passos em palavras, caso a animação não rode para você:
- O map guarda quatro pares. Cada par fica num bucket, escolhido pelo hash da sua chave.
m["cai"]passa"cai"pela função de hash. Digamos que o hash caia no bucket 2.- O map procura só no bucket 2. Compara as chaves, encontra
caie retorna 9 etrue. m["eve"]passa"eve"pelo hash. Digamos que caia no bucket 3.- O map procura só no bucket 3. Nenhuma das chaves ali é
eve, então ele retorna o zero value, 0, efalse. Nada é adicionado ao map.
O runtime de verdade é mais elaborado que o desenho. Desde o Go 1.24, maps são Swiss tables. As entradas ficam em grupos de oito slots, e cada grupo tem uma palavra de controle com um byte por slot. Esse byte guarda alguns bits do hash da chave, então o runtime consegue descartar a maioria dos slots sem comparar chave nenhuma. Maps grandes são divididos em várias tabelas que crescem de forma independente. Nada disso muda o que você vê no seu código: o hash decide onde procurar, e uma busca que não acha custa quase tão pouco quanto uma que acha.
Cada map também ganha a sua própria semente de hash aleatória. A mesma chave cai em lugares diferentes em dois maps diferentes, e em duas execuções diferentes do seu programa. É por isso que os números acima só podem ser ilustrativos, e é parte do motivo de a ordem de iteração não ser fixa.
Onde a analogia falha: uma caixa vazia de verdade seria um armário novo onde você poderia guardar coisas. Go não cria nada quando você lê uma chave ausente. Ele só devolve um zero value, e o map fica exatamente como estava.
Maps nil: ler funciona, escrever causa panic
O zero value de um tipo map é nil, e um map nil se comporta como um map vazio em tudo, menos em guardar uma chave:
package main
import "fmt"
func main() {
var prices map[string]float64
fmt.Println(prices == nil, len(prices), prices["tea"])
for k := range prices {
fmt.Println("never runs", k)
}
delete(prices, "tea")
prices["tea"] = 2.5
fmt.Println("never reached")
}
Imprime a primeira linha e para:
true 0 0
panic: assignment to entry in nil map
Ler de um map nil, percorrê-lo, pegar o tamanho e apagar dele, tudo isso funciona. São perguntas com resposta óbvia quando não há nada ali. Escrever é diferente. Uma escrita precisa de um lugar para colocar a entrada, e um map nil não tem tabela nenhuma.
Por que Go não aloca uma para você? Porque uma variável map guarda um ponteiro para a tabela do map. Se a escrita alocasse uma tabela nova, só a variável usada na escrita apontaria para ela. Qualquer cópia daquele map nil, como a que quem chamou passou para uma função, continuaria nil, e a entrada pareceria sumir. Um panic é mais barulhento e mais honesto.
A correção é criar o map antes de escrever nele, com um literal ou make:
package main
import "fmt"
func main() {
var prices map[string]float64
if prices == nil {
prices = make(map[string]float64)
}
prices["tea"] = 2.5
fmt.Println(prices)
}
Ele imprime:
map[tea:2.5]
Em código real, o map nil costuma estar escondido dentro de outra coisa, como um campo de struct que ninguém inicializou. var m map[K]V serve quando você só vai ler. Quando vai escrever, comece com m := map[K]V{} ou make.
A ordem de iteração não é uma ordem
Percorrer um map em Go visita cada chave exatamente uma vez, mas sem ordem fixa, e dois laços sobre o mesmo map na mesma execução podem discordar:
package main
import "fmt"
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}
for k := range m {
fmt.Println("loop 1:", k)
}
for k := range m {
fmt.Println("loop 2:", k)
}
}
Uma execução imprimiu:
loop 1: d
loop 1: e
loop 1: a
loop 1: b
loop 1: c
loop 2: a
loop 2: b
loop 2: c
loop 2: d
loop 2: e
O map não mudou entre os laços. Cada range escolheu o seu próprio ponto de partida aleatório.
Rodar isso no Go 1.26 revelou algo que vale saber. Com um map tão pequeno, as ordens muitas vezes parecem a ordem de inserção, rotacionada: a b c d e, depois d e a b c, depois c d e a b. Isso é um acaso de como uma Swiss table pequena guarda as entradas, e é exatamente o tipo de padrão em que as pessoas começam a confiar. A linguagem não promete nada sobre isso, e a próxima versão pode mudar.
Mudar o map enquanto você o percorre tem regras próprias. Apagar uma chave que você ainda não alcançou significa que você não vai vê-la, e apagar a chave em que você está é seguro. Uma chave adicionada durante o laço pode ou não ser visitada, então não conte com nenhuma das duas coisas. Apagar enquanto percorre é o caso comum e seguro:
package main
import "fmt"
func main() {
stock := map[string]int{"apples": 5, "bread": 0, "cheese": 7, "dates": 0}
for item, n := range stock {
if n == 0 {
delete(stock, item)
}
}
fmt.Println(stock)
}
Ele imprime:
map[apples:5 cheese:7]
Obtendo uma ordem estável
Quando a saída precisa de uma ordem, você mesmo a escolhe, normalmente ordenando as chaves. maps.Keys retorna um iterador sobre as chaves, e slices.Sorted as reúne num slice ordenado. Você também pode ordenar pelo valor:
package main
import (
"cmp"
"fmt"
"maps"
"slices"
"strings"
)
func main() {
votes := map[string]int{"tea": 4, "coffee": 7, "juice": 4, "water": 1}
fmt.Println(slices.Sorted(maps.Keys(votes)))
drinks := slices.Collect(maps.Keys(votes))
slices.SortFunc(drinks, func(a, b string) int {
return cmp.Or(
cmp.Compare(votes[b], votes[a]), // most votes first
strings.Compare(a, b), // then by name
)
})
fmt.Println(drinks)
}
Ele imprime:
[coffee juice tea water]
[coffee juice tea water]
A primeira linha está em ordem alfabética. A segunda ordenação coloca primeiro quem tem mais votos. tea e juice têm 4 cada, então o desempate pelo nome decide entre os dois. Sem desempate, duas execuções poderiam colocá-los em ordens diferentes, porque as chaves já chegaram numa ordem aleatória.
cmp.Or retorna o primeiro argumento que não é zero, e é isso que faz de “ordene por isto, depois por aquilo” uma única expressão.
Passando um map para uma função
Um map passado para uma função em Go não é copiado. A função recebe uma cópia da variável map, e essa cópia aponta para a mesma tabela, então toda mudança aparece do lado de fora:
package main
import "fmt"
func restock(m map[string]int, item string, n int) {
m[item] += n
}
func replace(m map[string]int) {
m = map[string]int{"surprise": 1}
fmt.Println("inside replace:", m)
}
func main() {
stock := map[string]int{"apples": 5}
restock(stock, "apples", 3)
restock(stock, "bread", 2)
replace(stock)
fmt.Println(stock)
}
Ele imprime:
inside replace: map[surprise:1]
map[apples:8 bread:2]
restock mudou um valor existente e adicionou uma chave nova, e quem chamou vê as duas coisas. Compare com slices. Uma função que faz append num slice que recebeu não consegue mudar o tamanho do slice de quem chamou, porque o tamanho fica no cabeçalho do slice que a função copiou. Um map guarda o seu tamanho dentro da tabela compartilhada, então adicionar chaves também aparece do lado de fora.
replace é o limite. Atribuir um map totalmente novo a m só muda a variável da própria função. Quem chamou continua apontando para a tabela antiga. Uma função que quer entregar um map diferente o retorna.
m[item] += n também funciona para uma chave que ainda não existe. A leitura dá 0, e a escrita guarda 0 mais n.
Você não pode mudar um campo de uma struct dentro de um map
Um valor guardado num map em Go não é endereçável, então você não pode atribuir a uma parte dele diretamente:
package main
import "fmt"
type player struct {
name string
score int
}
func main() {
players := map[string]player{"ana": {name: "Ana"}}
players["ana"].score = 10
fmt.Println(players)
}
O build falha com:
./main.go:12:2: cannot assign to struct field players["ana"].score in map
O motivo é o crescimento que você viu na seção sobre busca. Quando um map cresce, o runtime move as entradas para lugares novos. Se Go deixasse você guardar um ponteiro para um valor dentro do map, esse ponteiro poderia acabar apontando para um slot que o map já abandonou. Por isso a linguagem não deixa você pegar o endereço de um valor de map, e atribuir a um dos campos dele precisaria exatamente disso. &players["ana"] é recusado pelo mesmo motivo.
Há duas correções. Leia o valor, mude a cópia e guarde de volta, ou guarde ponteiros no map:
package main
import "fmt"
type player struct {
name string
score int
}
func main() {
players := map[string]player{"ana": {name: "Ana"}}
p := players["ana"]
p.score = 10
players["ana"] = p
byPointer := map[string]*player{"ben": {name: "Ben"}}
byPointer["ben"].score = 7
fmt.Println(players["ana"].score, byPointer["ben"].score)
}
Ele imprime:
10 7
A versão que copia e guarda mantém o map como único dono dos seus valores. A versão com ponteiros é mais curta quando você atualiza com frequência, mas tem uma armadilha: o zero value de um ponteiro é nil, então byPointer["zed"].score = 1 numa chave ausente causa panic. A parte sobre structs, métodos e ponteiros mostra quando escolher cada uma.
O que pode ser chave
Uma chave de map em Go precisa ser comparável, ou seja, == precisa funcionar nela, porque o map tem que conferir se duas chaves são a mesma. Slices, maps e funções não podem ser comparados com ==, então não podem ser chaves:
package main
import "fmt"
func main() {
seen := map[[]string]bool{}
fmt.Println(seen)
}
O build falha com:
./main.go:6:14: invalid map key type []string
Strings, números, booleanos, ponteiros e channels funcionam. Arrays e structs também, desde que cada elemento ou campo também seja comparável. Uma chave struct é o jeito limpo de indexar um map por mais de uma coisa:
package main
import "fmt"
type cell struct {
row, col int
}
func main() {
board := map[cell]string{}
board[cell{0, 0}] = "X"
board[cell{1, 2}] = "O"
fmt.Println(board[cell{1, 2}], len(board))
_, taken := board[cell{2, 2}]
fmt.Println("2,2 taken:", taken)
trips := map[[2]string]int{}
trips[[2]string{"home", "work"}]++
trips[[2]string{"home", "work"}]++
trips[[2]string{"work", "gym"}]++
fmt.Println(trips[[2]string{"home", "work"}], len(trips))
}
Ele imprime:
O 2
2,2 taken: false
2 2
cell{1, 2} construído em dois lugares diferentes é a mesma chave, porque duas structs são iguais quando todos os campos são iguais. Você não precisa colar a linha e a coluna numa string como "1,2".
Um tipo de chave passa pelo compilador e falha depois. Com uma chave interface como any, o compilador não tem como saber o que você vai guardar, então um slice escapa até o programa rodar:
package main
import "fmt"
func main() {
seen := map[any]bool{}
seen[42] = true
seen["42"] = true
fmt.Println(len(seen))
seen[[]int{4, 2}] = true
}
Imprime a primeira linha e para:
2
panic: runtime error: hash of unhashable type []int
42 e "42" são chaves diferentes, porque os tipos são diferentes. O slice não pode passar por hash de jeito nenhum, e essa verificação só pode acontecer em tempo de execução.
Exemplo prático: contando palavras
Contar quantas vezes cada palavra aparece é o trabalho clássico de um map, e o zero value transforma isso numa única linha dentro do laço:
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
func main() {
text := "the cat sat on the mat and the cat slept"
counts := map[string]int{}
for _, word := range strings.Fields(text) {
counts[word]++
}
for _, word := range slices.Sorted(maps.Keys(counts)) {
fmt.Println(word, counts[word])
}
}
Ele imprime:
and 1
cat 2
mat 1
on 1
sat 1
slept 1
the 3
counts[word]++ lê a contagem atual, que é 0 na primeira vez que uma palavra aparece, soma um e guarda. Não é preciso nenhuma checagem de “se a chave existe”.
Exemplo prático: agrupando em slices
Agrupar valores sob uma chave é o outro trabalho do dia a dia, e um map[string][]string resolve isso com append:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
fruit := []string{"apple", "banana", "avocado", "cherry", "blueberry", "apricot"}
byLetter := map[string][]string{}
for _, f := range fruit {
letter := f[:1]
byLetter[letter] = append(byLetter[letter], f)
}
for _, letter := range slices.Sorted(maps.Keys(byLetter)) {
fmt.Println(letter, byLetter[letter])
}
}
Ele imprime:
a [apple avocado apricot]
b [banana blueberry]
c [cherry]
Na primeira vez que uma letra aparece, byLetter[letter] é um slice nil. append num slice nil funciona, então o grupo começa sozinho. Você precisa guardar o resultado de volta com byLetter[letter] = ..., pelo mesmo motivo que escreve s = append(s, x): append pode retornar um slice sobre um array novo.
Dentro de cada grupo, as frutas ficam na ordem de entrada, porque um slice mantém a sua ordem. Só as chaves precisaram ser ordenadas.
O pacote maps e clear
O pacote maps da biblioteca padrão, adicionado no Go 1.21, cobre as tarefas para as quais você escreveria laços:
package main
import (
"fmt"
"maps"
)
func main() {
prices := map[string]int{"tea": 3, "coffee": 4, "cake": 5}
backup := maps.Clone(prices)
prices["tea"] = 99
fmt.Println(backup["tea"], maps.Equal(prices, backup))
maps.DeleteFunc(prices, func(item string, price int) bool {
return price > 4
})
fmt.Println(prices)
clear(prices)
fmt.Println(prices, len(prices), prices == nil)
}
Ele imprime:
3 false
map[coffee:4]
map[] 0 false
maps.Clone cria um map novo com as mesmas chaves e valores, então mudar prices não mexeu em backup. Mas o clone é raso. Se os valores forem slices ou ponteiros, os dois maps compartilham aquilo para onde eles apontam.
maps.Equal diz se dois maps têm as mesmas chaves com os mesmos valores. Você precisa dele porque == não funciona entre dois maps. A única coisa com que um map se compara é nil.
maps.DeleteFunc remove toda entrada para a qual a função retorna true, que é o laço da seção sobre iteração numa única chamada. O bolo custava 5, e o chá agora custa 99, então os dois saíram.
clear, uma função embutida desde o Go 1.21, remove todas as entradas. O map fica vazio mas continua utilizável, e não é nil.
clear também faz uma coisa que delete não faz. Um NaN de ponto flutuante nunca é igual a nada, nem a si mesmo, então uma chave NaN pode entrar mas nunca mais ser encontrada:
package main
import (
"fmt"
"math"
)
func main() {
m := map[float64]string{}
m[math.NaN()] = "first"
m[math.NaN()] = "second"
fmt.Println(len(m))
delete(m, math.NaN())
fmt.Println(len(m))
clear(m)
fmt.Println(len(m))
}
Ele imprime:
2
2
0
Duas escritas na “mesma” chave NaN criaram duas entradas, e delete não conseguiu encontrar nenhuma delas. clear não procura chaves, então remove todas mesmo assim. Você raramente vai indexar um map por floats, mas quando fizer isso, é por isso.
Maps e goroutines
Um map comum em Go não é seguro para uso por várias goroutines ao mesmo tempo, e quando duas goroutines escrevem nele simultaneamente o runtime pode derrubar o programa inteiro com fatal error: concurrent map writes, o que a parte sobre sync e o race detector mostra como evitar.
O que lembrar
m[k] = vadiciona ou substitui,delete(m, k)remove, elen(m)conta. Ler uma chave ausente retorna o zero value e não a adiciona.- Use
v, ok := m[k]sempre que o zero value puder ser um valor de verdade. - Um map nil pode ser lido, percorrido e ter chaves apagadas, mas escrever nele causa panic. Crie-o antes com um literal ou
make. - A ordem de iteração nunca é garantida, mesmo quando um map pequeno parece manter uma. Ordene as chaves com
slices.Sorted(maps.Keys(m))quando a ordem importar. - Um map passado para uma função compartilha a tabela, então chaves novas e valores alterados aparecem do lado de fora.
- Valores de map não são endereçáveis. Copie, mude e guarde de volta, ou guarde ponteiros. Chaves precisam ser comparáveis, e uma struct é uma boa chave de várias partes.
maps.Clone,maps.Equal,maps.DeleteFunceclearsubstituem a maioria dos laços escritos à mão sobre maps.
Um map encontra um valor pelo lugar onde a chave cai no hash, então ele não pode dar uma ordem, só uma resposta.