O que é Map em Go?

Um map em Go é uma estrutura de dados nativa que implementa uma tabela hash (hash table), associando chaves a valores. É o equivalente ao dicionário em Python, HashMap em Java ou objeto/Map em JavaScript. Maps permitem inserção, busca e remoção em tempo médio O(1), tornando-os ideais para cenários que exigem acesso rápido por chave.

Maps são uma das estruturas mais usadas no desenvolvimento Go — desde configurações de aplicação, caches em memória, contadores de frequência, até indexação de dados retornados por bancos. Junto com slices e structs, maps formam o trio fundamental de estruturas de dados em Go.

O tipo de um map é escrito como map[KeyType]ValueType, onde KeyType deve ser um tipo comparável (que suporta ==) e ValueType pode ser qualquer tipo, inclusive outro map ou um slice.

Declarando e inicializando maps

Literal de map

// Map de string para int
idades := map[string]int{
    "Alice":  30,
    "Bruno":  25,
    "Carla":  28,
}

// Map vazio (não nil)
config := map[string]string{}

Usando make()

// Map vazio com capacity hint
usuarios := make(map[int]string)

// Com capacity pré-definida (otimização)
cache := make(map[string][]byte, 1000)

O segundo argumento de make() para maps é um hint de capacidade inicial — não um limite. O map cresce automaticamente conforme necessário, mas pré-alocar evita rehashing em cenários onde você sabe o volume de dados.

Nil map vs map vazio

var nilMap map[string]int    // nil — leitura ok, escrita panic!
vazioMap := map[string]int{} // vazio — leitura e escrita ok

// Leitura em nil map retorna zero value
fmt.Println(nilMap["chave"]) // 0 (sem panic)

// Escrita em nil map causa panic!
// nilMap["chave"] = 1 // panic: assignment to entry in nil map

Sempre inicialize um map antes de escrever nele. Essa é uma fonte comum de bugs em Go, especialmente quando maps são campos de structs não inicializados.

Operações CRUD com maps

Create/Update — Inserindo e atualizando

scores := make(map[string]int)

// Inserir
scores["Go"] = 95
scores["Python"] = 88

// Atualizar (mesma sintaxe)
scores["Go"] = 97

Read — Lendo valores

valor := scores["Go"] // 97

// Chave inexistente retorna zero value do tipo
inexistente := scores["Java"] // 0 (não panic)

O idioma comma ok

O comma ok é essencial para distinguir entre um valor zero legítimo e uma chave ausente:

valor, ok := scores["Java"]
if ok {
    fmt.Printf("Java: %d\n", valor)
} else {
    fmt.Println("Java não encontrado no map")
}

// Padrão idiomático compacto
if v, ok := scores["Go"]; ok {
    fmt.Printf("Go score: %d\n", v)
}

Esse padrão é análogo ao type assertion com comma ok usado em interfaces.

Delete — Removendo entradas

delete(scores, "Python") // Remove a chave "Python"
delete(scores, "inexistente") // Não causa panic — é um no-op

A função delete() é segura — chamar com uma chave inexistente ou em um nil map não causa panic.

Iterando sobre maps

capitais := map[string]string{
    "Brasil":    "Brasília",
    "Argentina": "Buenos Aires",
    "Chile":     "Santiago",
}

for pais, capital := range capitais {
    fmt.Printf("%s → %s\n", pais, capital)
}

Ordem de iteração é aleatória

Go randomiza propositalmente a ordem de iteração de maps a cada execução. Isso é uma decisão de design para evitar que desenvolvedores dependam de uma ordem específica. Se precisar de ordem determinística:

import "sort"

// Coletar chaves, ordenar, iterar na ordem
chaves := make([]string, 0, len(capitais))
for k := range capitais {
    chaves = append(chaves, k)
}
sort.Strings(chaves)

for _, k := range chaves {
    fmt.Printf("%s → %s\n", k, capitais[k])
}

Maps e concorrência

Maps em Go não são seguros para acesso concorrente. Se múltiplas goroutines acessam o mesmo map (e pelo menos uma escreve), você terá uma race condition que pode causar crash com a mensagem fatal: concurrent map writes.

Proteção com Mutex

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

func (c *CacheSeguro) Get(chave string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    valor, ok := c.dados[chave]
    return valor, ok
}

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

O uso de defer para unlock é uma prática recomendada — veja mais em defer no glossário.

sync.Map — Map concorrente da standard library

Para cenários específicos de alta concorrência, Go oferece sync.Map:

var sm sync.Map

// Store
sm.Store("chave", "valor")

// Load
if valor, ok := sm.Load("chave"); ok {
    fmt.Println(valor.(string))
}

// LoadOrStore — carrega se existe, senão armazena
actual, loaded := sm.LoadOrStore("nova", "default")

// Delete
sm.Delete("chave")

// Range
sm.Range(func(key, value any) bool {
    fmt.Printf("%v: %v\n", key, value)
    return true // continue iterating
})

sync.Map é otimizado para dois cenários: (1) chaves escritas uma vez e lidas muitas vezes, e (2) múltiplas goroutines lendo/escrevendo conjuntos disjuntos de chaves. Fora esses casos, sync.RWMutex + map regular geralmente tem melhor performance.

Tipos de chave válidos

Qualquer tipo comparável pode ser chave de map:

// Struct como chave
type Coordenada struct {
    Lat, Lng float64
}
pontos := map[Coordenada]string{
    {-23.55, -46.63}: "São Paulo",
    {-22.90, -43.17}: "Rio de Janeiro",
}

// Array (tamanho fixo) como chave — ok
m1 := map[[2]int]string{}

// Slice como chave — NÃO COMPILA
// m2 := map[[]int]string{} // error: invalid map key type

Tipos que não podem ser chaves: slices, maps e funções — pois não são comparáveis com ==.

Performance: map vs slice

OperaçãoMapSlice
Busca por valorO(1) médioO(n)
InserçãoO(1) amortizadoO(1) amortizado (append)
RemoçãoO(1)O(n)
Iteração ordenadaO(n log n) + sortO(n)
Uso de memóriaMaior (overhead de hash)Menor

Para coleções pequenas (< 20 elementos), um slice com busca linear pode ser mais rápido que um map devido ao menor overhead de memória e melhor localidade de cache. Para coleções maiores ou quando busca por chave é frequente, maps são superiores.

Para entender como otimizar essas escolhas, consulte o tutorial de profiling em Go.

Padrões comuns com maps

Contadores de frequência

func contarPalavras(texto string) map[string]int {
    palavras := strings.Fields(texto)
    contagem := make(map[string]int, len(palavras))
    for _, p := range palavras {
        contagem[strings.ToLower(p)]++
    }
    return contagem
}

Set (conjunto) usando map

Go não tem tipo nativo de set, mas maps com struct{} como valor resolvem:

type Set map[string]struct{}

func (s Set) Add(item string)          { s[item] = struct{}{} }
func (s Set) Contains(item string) bool { _, ok := s[item]; return ok }
func (s Set) Remove(item string)       { delete(s, item) }

Cache simples

type Cache struct {
    dados map[string]resultado
    mu    sync.RWMutex
}

func (c *Cache) GetOrCompute(chave string, fn func() resultado) resultado {
    c.mu.RLock()
    if v, ok := c.dados[chave]; ok {
        c.mu.RUnlock()
        return v
    }
    c.mu.RUnlock()

    c.mu.Lock()
    defer c.mu.Unlock()
    // Double-check após adquirir write lock
    if v, ok := c.dados[chave]; ok {
        return v
    }
    v := fn()
    c.dados[chave] = v
    return v
}

Para implementações de cache mais robustas com TTL e eviction, veja o tutorial de Redis com Go.

Boas práticas com maps

  1. Sempre inicialize maps antes de escrever — nil maps causam panic
  2. Use comma ok para distinguir zero values de chaves ausentes
  3. Pré-aloque capacity com make(map[K]V, n) quando souber o volume
  4. Proteja acesso concorrente com mutex ou channels
  5. Prefira struct keys a strings concatenadas para chaves compostas
  6. Não dependa da ordem de iteração — ela é randomizada

Para aprofundar seus conhecimentos sobre Go, explore os tutoriais práticos e o guia de módulos Go.

Perguntas frequentes (FAQ)

Qual a diferença entre nil map e map vazio em Go?

Um nil map (var m map[string]int) permite leitura (retorna zero value) mas causa panic na escrita. Um map vazio (map[string]int{} ou make(map[string]int)) permite leitura e escrita normalmente. Sempre inicialize com make() ou literal antes de inserir dados.

Maps em Go são thread-safe?

Não. Maps nativos em Go não são seguros para acesso concorrente. Se múltiplas goroutines acessam o mesmo map e pelo menos uma escreve, use sync.RWMutex para proteger o acesso ou sync.Map para cenários de alta concorrência com padrões específicos de leitura/escrita.

Como verificar se uma chave existe em um map?

Use o idioma comma ok: valor, ok := meuMap[chave]. A variável ok será true se a chave existe e false caso contrário. Isso é essencial porque o acesso a uma chave inexistente retorna o zero value do tipo, que pode ser um valor legítimo (como 0 para int ou "" para string).

Quando usar sync.Map ao invés de mutex com map regular?

Use sync.Map em dois cenários: quando chaves são escritas uma vez e lidas muitas vezes (cache de configuração) ou quando goroutines operam em conjuntos disjuntos de chaves. Para todos os outros casos — especialmente quando há frequência similar de leitura e escrita — um sync.RWMutex com map regular oferece melhor performance.