A palavra-chave map declara a tabela de hash nativa do Go. O tipo map[K]V associa chaves do tipo K a valores do tipo V, com pesquisa, inserção e remoção em tempo constante no caso médio. Os maps fazem parte da própria linguagem, por isso não precisas de importar um pacote nem de instanciar uma classe HashMap. Tens uma sintaxe literal, as funções nativas make, delete, clear e len, e suporte para for range (especificação do Go).
Resumo rápido
map[string]int{"/healthz": 1}oumake(map[string]int)cria um map.var m map[string]intdá-te um mapnil.- Ler de um map
nildevolve o valor zero. Escrever num entra em pânico comassignment to entry in nil map. - Uma chave inexistente devolve o valor zero. Usa
v, ok := m[k]quando precisas de saber se a chave lá estava. - A ordem de iteração não está especificada e é aleatória. Ordena as chaves com
slices.Sorted(maps.Keys(m))quando a ordem importa. - As chaves têm de ser comparáveis. Strings, números, structs e arrays funcionam. Slices, maps e funções não.
- Um valor de map é um pequeno descritor. Passá-lo a uma função permite que a função modifique o map de quem a chamou.
m[k].Field = xnão compila quando o valor é uma struct. Copia, modifica e volta a guardar, ou guarda ponteiros.- Os maps não são seguros para escritas concorrentes. Protege-os com um
sync.Mutexou usasync.Mapnos casos para que foi criado. map[string]struct{}é o set do Go. O pacotemaps(Go 1.21+) acrescentaClone,Equal,DeleteFunc,KeyseCollect.
Como se declara e cria um map em Go?
Um tipo map escreve-se map[KeyType]ValueType. Crias um map com um literal composto quando já conheces algumas entradas, ou com make quando começas vazio:
example.gogopackage main import "fmt" func main() { statusText := map[int]string{ 200: "OK", 404: "Not Found", 503: "Service Unavailable", } hits := make(map[string]int) hits["/api/orders"]++ hits["/api/orders"]++ hits["/healthz"]++ fmt.Println(statusText[404], len(statusText)) fmt.Println(hits["/api/orders"], hits["/healthz"], len(hits)) }
example.texttextNot Found 3 2 1 2
hits["/api/orders"]++ funciona com uma chave que ainda não existe. A leitura devolve 0, o incremento passa-o a 1 e a escrita guarda-o. Um contador não precisa de preparação nenhuma.
O make aceita uma indicação de tamanho opcional, como em make(map[string]int, len(requests)). O runtime reserva espaço para cerca desse número de entradas, por isso o map não precisa de crescer enquanto o preenches. A indicação não é um limite. O map continua a crescer para além dela. len(m) devolve o número de entradas, e cap não funciona com maps.
var vs make em Go compara as duas formas de obter um map vazio e explica porque é que o var sozinho não chega.
Porque é que escrever num map nil entra em pânico?
O valor zero de um tipo map é nil. Um map nil não tem nenhuma tabela de hash por trás, por isso não há onde pôr uma entrada nova. Isto costuma aparecer como um campo map numa struct que ninguém inicializou:
example.gogopackage main import "fmt" type Router struct { routes map[string]string } func main() { var r Router fmt.Println(r.routes["/health"] == "", len(r.routes)) r.routes["/health"] = "healthHandler" }
example.texttexttrue 0 panic: assignment to entry in nil map
A leitura funciona. Um map nil comporta-se como um map vazio em pesquisas, len, range e delete, por isso a primeira linha imprime normalmente. Só a escrita entra em pânico. A correção é criar o map antes da primeira escrita, normalmente num construtor:
example.gogofunc NewRouter() *Router { return &Router{routes: make(map[string]string)} }
Um descodificador como o json.Unmarshal aloca o map por ti quando encontra um objeto JSON, por isso um destino map[string]any pode começar como nil. O teu próprio código tem de chamar make ou usar um literal.
Como verificas se uma chave existe num map em Go?
Indexar um map com uma chave inexistente devolve o valor zero do tipo do valor. Para um contador é exatamente o que queres, já que um caminho que ninguém pediu tem 0 acessos. Para outros valores, o valor zero esconde uma diferença real. Um map de feature flags devolve false para uma flag desligada e também para uma flag que nunca foi definida.
A forma comma-ok devolve um segundo booleano que diz se a chave estava presente:
example.gogopackage main import "fmt" func main() { flags := map[string]bool{ "new_checkout": true, "dark_mode": false, } for _, name := range []string{"new_checkout", "dark_mode", "beta_search"} { enabled, ok := flags[name] if !ok { fmt.Printf("%s: unknown flag\n", name) continue } fmt.Printf("%s: %v\n", name, enabled) } }
example.texttextnew_checkout: true dark_mode: false beta_search: unknown flag
O ok só é false para beta_search. Sem ele, um erro de escrita no nome de uma flag desligaria a funcionalidade sem ninguém dar por isso. Usa a forma simples quando o valor zero é uma resposta correta, como em contagens e somas. Usa comma-ok quando «em falta» significa algo diferente de «zero».
Como se removem chaves e se limpa um map?
delete(m, k) remove uma chave. Remover uma chave que não existe não faz nada e não é um erro. clear(m), acrescentado no Go 1.21, remove todas as entradas e mantém o map alocado (notas de lançamento do Go 1.21):
example.gogocache := map[int]string{42: "[email protected]", 7: "[email protected]"} delete(cache, 42) delete(cache, 999) fmt.Println(len(cache), cache) clear(cache) fmt.Println(len(cache), cache == nil)
example.texttext1 map[7:[email protected]] 0 false
Depois do clear, o map fica vazio mas continua utilizável, e todas as variáveis que apontam para ele veem o map vazio. Atribuir cache = map[int]string{} dá um map novo apenas a essa variável.
Remover chaves enquanto percorres o mesmo map com range é permitido. A especificação diz que uma entrada removida durante a iteração não volta a aparecer mais à frente no ciclo (especificação do Go). Acrescentar chaves durante a iteração também é permitido, mas uma entrada nova pode aparecer ou não no mesmo ciclo.
Porque é que a ordem de iteração de um map é aleatória em Go?
A especificação diz que a ordem de iteração sobre um map não está definida e que não há garantia de ser igual de uma iteração para a seguinte. O runtime vai mais longe e escolhe um ponto de partida aleatório em cada range. Três execuções do mesmo ciclo sobre hits imprimiram:
example.texttext/api/orders 41 /healthz 120 /api/users 17 /api/orders 41 /healthz 120 /api/users 17 /api/users 17 /api/orders 41 /healthz 120
A aleatoriedade é intencional. As primeiras versões do Go tinham uma ordem que parecia estável em maps pequenos, e os programas começaram a depender dela. Quando a implementação mudou, esses programas deixaram de funcionar. Tornar a ordem aleatória faz o bug aparecer nos teus testes em vez de em produção (blog do Go, Go maps in action).
Quando precisas de uma ordem estável, para uma linha de log, um relatório ou um teste, ordena as chaves. Desde o Go 1.23, maps.Keys devolve um iterador e slices.Sorted recolhe-o e ordena-o numa só chamada:
example.gogopackage main import ( "fmt" "maps" "slices" ) func main() { hits := map[string]int{"/api/orders": 41, "/healthz": 120, "/api/users": 17} for _, path := range slices.Sorted(maps.Keys(hits)) { fmt.Printf("%-12s %d\n", path, hits[path]) } }
example.texttext/api/orders 41 /api/users 17 /healthz 120
O fmt.Println de um map inteiro já imprime as chaves ordenadas, e é por isso que a saída de fmt.Println(cache) mais acima é estável. O encoding/json também ordena as chaves do map ao serializar. Só o range é aleatório.
Que tipos podem ser chaves de um map em Go?
Um tipo de chave tem de ser comparável, ou seja, == e != têm de estar definidos para ele. Strings, inteiros, floats, booleanos, ponteiros, canais e interfaces são comparáveis. O mesmo acontece com structs e arrays cujos campos ou elementos sejam todos comparáveis. Slices, maps e funções não são, e o compilador rejeita-os:
example.gogoseen := map[[]string]bool{}
example.texttext./main.go:4:14: invalid map key type []string
As chaves struct são úteis quando a chave tem mais do que uma parte. Aqui, a latência de cada endpoint tem como chave o método e o caminho em conjunto:
example.gogotype Endpoint struct { Method string Path string } latencyMs := map[Endpoint]int{ {"GET", "/api/orders"}: 38, {"POST", "/api/orders"}: 112, } fmt.Println(latencyMs[Endpoint{"POST", "/api/orders"}])
Isto imprime 112. A struct é comparada campo a campo, por isso dois valores Endpoint com o mesmo método e caminho encontram a mesma entrada. Evita o truque antigo de construir uma string de chave como "POST /api/orders" e voltar a dividi-la mais tarde. Para usar uma lista de IDs como chave, converte o slice num array de tamanho fixo ou junta-o numa string.
Chaves interface como map[any]int compilam, mas inserir um slice em tempo de execução entra em pânico com runtime error: hash of unhashable type []string. Chaves float funcionam mas são má ideia. NaN não é igual a si próprio, por isso cada m[math.NaN()] = 1 acrescenta uma entrada nova que nunca mais consegues encontrar.
Os maps são passados por referência em Go?
O Go passa tudo por valor, incluindo os maps. O valor de uma variável map é um pequeno descritor que aponta para a tabela de hash, por isso copiá-lo copia o ponteiro e não as entradas. Uma função que recebe um map pode alterar as entradas de quem a chamou. Uma função que atribui um map novo ao seu parâmetro só altera a sua própria cópia:
example.gogofunc recordStatus(counts map[int]int, status int) { counts[status]++ } func resetCounts(counts map[int]int) { counts = map[int]int{} } func main() { counts := map[int]int{} recordStatus(counts, 200) recordStatus(counts, 500) resetCounts(counts) fmt.Println(counts) }
example.texttextmap[200:1 500:1]
O recordStatus escreveu através da tabela partilhada, por isso as duas contagens estão lá. O resetCounts substituiu a sua variável local e não teve efeito em quem o chamou. Para esvaziar o map de quem chama, usa clear(counts) dentro da função. O go vet não assinala isto, por isso a segunda função compila e não faz nada.
A mesma partilha aplica-se a campos de structs e a valores devolvidos. Um getter que devolve um map interno permite que quem o chama modifique o teu estado. Devolve maps.Clone(m) quando quem chama deve receber a sua própria cópia.
Porque é que não consigo atribuir a um campo de struct dentro de um map?
Um valor de map não é endereçável. O runtime pode mover entradas quando a tabela cresce, por isso o Go não te dá uma referência para um valor guardado lá dentro. É por isso que isto não compila:
example.gogotype Session struct { UserID int Requests int } sessions := map[string]Session{"s_91": {UserID: 42}} sessions["s_91"].Requests++
example.texttext./main.go:10:2: cannot assign to struct field sessions["s_91"].Requests in map
Há duas correções. Copia o valor para fora, altera-o e volta a guardá-lo. Ou guarda ponteiros, para que o map contenha endereços e a struct viva fora da tabela:
example.gogos := sessions["s_91"] s.Requests++ sessions["s_91"] = s byToken := map[string]*Session{"s_91": {UserID: 42}} byToken["s_91"].Requests++
As duas deixam Requests a 1. A forma de copiar e guardar mantém o map como único dono dos dados. A forma com ponteiros é mais curta de atualizar e evita copiar structs grandes, mas uma chave inexistente dá-te um ponteiro nil, por isso byToken["s_404"].Requests++ entra em pânico. Verifica primeiro com comma-ok. A mesma regra explica porque não podes fazer &sessions["s_91"].
Os maps em Go são seguros para uso concorrente?
Não. Leituras concorrentes não são problema, mas uma escrita em simultâneo com qualquer outra leitura ou escrita é uma data race. O runtime deteta algumas destas races e para o programa inteiro com um erro fatal que o recover não consegue apanhar. Oito goroutines a incrementar um map de contadores partilhado rebentam logo:
example.gogohits := map[string]int{} var wg sync.WaitGroup for range 8 { wg.Go(func() { for range 10000 { hits["/api/orders"]++ } }) } wg.Wait()
example.texttextfatal error: concurrent map writes
Uma leitura em corrida com uma escrita dá fatal error: concurrent map read and map write. Num servidor HTTP, isto acontece assim que dois pedidos tocam num map partilhado, já que cada pedido corre na sua própria goroutine. Corre os teus testes com go test -race para apanhar as races que a verificação do runtime deixa escapar.
A correção habitual é pôr o map e um sync.Mutex num só tipo e só mexer no map através de métodos que fazem lock:
example.gogotype HitCounter struct { mu sync.Mutex hits map[string]int } func NewHitCounter() *HitCounter { return &HitCounter{hits: make(map[string]int)} } func (c *HitCounter) Inc(path string) { c.mu.Lock() defer c.mu.Unlock() c.hits[path]++ } func (c *HitCounter) Get(path string) int { c.mu.Lock() defer c.mu.Unlock() return c.hits[path] }
As mesmas oito goroutines terminam agora e c.Get("/api/orders") devolve 80000. Usa sync.RWMutex com RLock no Get quando as leituras são muito mais numerosas do que as escritas e cada leitura faz trabalho a sério.
O sync.Map é um map concorrente da biblioteca padrão. A documentação diz que está otimizado para dois casos: chaves escritas uma vez e lidas muitas vezes, como uma cache que só cresce, e goroutines que trabalham sobre conjuntos de chaves disjuntos (documentação do sync.Map). Fora desses casos, um map normal com um mutex costuma ser mais simples e igualmente rápido, e mantém os teus tipos de chave e de valor em vez de any.
Como se cria um set em Go?
O Go não tem um tipo set. O idioma é um map com uma struct vazia como valor. struct{} ocupa zero bytes, por isso o map guarda apenas chaves. Este código descarta pedidos duplicados com base no ID:
example.gogorequestIDs := []string{"req_7f", "req_a1", "req_7f", "req_c3", "req_a1"} seen := make(map[string]struct{}, len(requestIDs)) for _, id := range requestIDs { if _, dup := seen[id]; dup { fmt.Println("duplicate, skipping", id) continue } seen[id] = struct{}{} } fmt.Println(len(seen), "unique requests")
example.texttextduplicate, skipping req_7f duplicate, skipping req_a1 3 unique requests
map[string]bool também funciona e lê-se um pouco melhor, já que if seen[id] dispensa o comma-ok. Custa um byte por entrada e permite um terceiro estado, uma chave guardada com false. Escolhe uma das formas para todo o projeto. A palavra-chave struct em Go aborda o struct{} e o seu outro uso como sinal em canais.
O que faz o pacote maps?
O pacote maps chegou no Go 1.21 com funções genéricas auxiliares, e o Go 1.23 acrescentou-lhe funções de iteradores (pkg.go.dev/maps). Estas são as que aparecem mais vezes:
example.gogodefaults := map[string]string{"region": "eu-west-1", "log_level": "info"} cfg := maps.Clone(defaults) cfg["log_level"] = "debug" cfg["debug_token"] = "tok_123" fmt.Println(maps.Equal(cfg, defaults)) maps.DeleteFunc(cfg, func(k, v string) bool { return strings.HasPrefix(k, "debug_") }) fmt.Println(slices.Sorted(maps.Keys(cfg))) fmt.Println(defaults["log_level"]) roles := maps.Collect(slices.All([]string{"admin", "editor", "viewer"})) fmt.Println(roles[1])
example.texttextfalse [log_level region] info editor
maps.Clonefaz uma cópia superficial. Alterarcfgdeixoudefaultsintacto. Os próprios valores não são copiados em profundidade, por isso um map de slices continua a partilhar os slices.maps.Equalcompara chaves e valores com==. Não podes comparar dois maps diretamente com==, já que os maps só podem ser comparados comnil.maps.DeleteFuncremove todas as entradas que satisfazem um predicado.maps.Keys,maps.Valuesemaps.Alldevolvem iteradores. Passa-os aslices.Sorted, aslices.Collectou a um ciclofor range.maps.Collectconstrói um map a partir de qualquer iterador de pares chave-valor. Aqui transforma um slice num map de índice para valor.maps.Copy(dst, src)funde um map noutro.
Como são implementados os maps em Go?
Desde o Go 1.24, o map nativo usa um design Swiss Table, uma tabela de hash com endereçamento aberto que verifica grupos de posições de uma só vez. As notas de lançamento indicam um menor custo de CPU nas operações sobre maps (notas de lançamento do Go 1.24). A linguagem não mudou, por isso o código escrito para os antigos maps baseados em buckets corre como antes.
Os maps continuam a nunca encolher. Remover entradas liberta as posições para reutilização mas mantém a memória. Uma cache que chegou a ter um milhão de sessões mantém essa memória depois de as removeres. O clear também não a liberta. Se um map de longa duração tiver um pico e depois descer, copia as entradas que restam para um map novo com maps.Clone ou com um make novo e deixa o antigo ser recolhido.
Onde entra o LevelUpGo
O LevelUpGo ensina Go através de exercícios que executam código Go real no navegador. O Composite Types aborda os maps ao lado dos slices e das structs, incluindo pesquisas com comma-ok, contagens, remoção e iteração por uma ordem estável. O Concurrency Fundamentals aborda goroutines, WaitGroups e os mutexes de que precisas quando um map é partilhado entre elas. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.
Perguntas frequentes
O map é uma palavra-chave em Go?
Sim. O map é uma das 25 palavras-chave reservadas do Go, por isso não o podes usar como nome de variável ou de função. Aparece em tipos map como map[string]int, em literais de map e como argumento de make.
Um map em Go é ordenado?
Não. A especificação deixa a ordem de iteração por definir, e o runtime torna-a aleatória em cada range. Para processar as chaves por ordem, ordena-as com slices.Sorted(maps.Keys(m)). O fmt.Println e o json.Marshal já imprimem as chaves do map ordenadas.
Os maps em Go são thread-safe?
Não. As leituras concorrentes são seguras, mas qualquer escrita em simultâneo com outra leitura ou escrita pode fazer o programa rebentar com fatal error: concurrent map writes. Protege o map com um sync.Mutex ou sync.RWMutex, ou usa sync.Map para caches escritas uma só vez e conjuntos de chaves disjuntos.
Como se obtém o tamanho de um map em Go?
Chama len(m). Devolve o número de entradas e funciona com um map nil, onde devolve 0. Os maps não suportam cap.
O Go tem um tipo set?
Não. Usa map[T]struct{}, que guarda apenas chaves, e verifica se um elemento pertence ao set com _, ok := set[k]. map[T]bool também funciona se preferires if set[k] ao comma-ok.
O que acontece quando lês uma chave que não existe?
Recebes o valor zero do tipo do valor, como 0, "", false ou nil, e nenhum erro. Usa a forma com dois valores v, ok := m[k] quando precisas de distinguir uma chave em falta de um valor zero guardado.
Fontes
- The Go Programming Language Specification, Map types: https://go.dev/ref/spec#Map_types
- The Go Programming Language Specification, Index expressions: https://go.dev/ref/spec#Index_expressions
- The Go Programming Language Specification, Deletion of map elements: https://go.dev/ref/spec#Deletion_of_map_elements
- The Go Programming Language Specification, For statements with range clause: https://go.dev/ref/spec#For_range
- The Go Blog, Go maps in action: https://go.dev/blog/maps
- Go 1.21 release notes: https://go.dev/doc/go1.21
- Go 1.23 release notes: https://go.dev/doc/go1.23
- Go 1.24 release notes: https://go.dev/doc/go1.24
- maps package: https://pkg.go.dev/maps
- sync.Map: https://pkg.go.dev/sync#Map
