Pular para o conteúdo

Trilha de Go · parte 2 de 37

Anatomia de Slices, Arrays e Maps em Go: Estruturas Internas e Armadilhas

Por dentro de arrays, slices e maps em Go: o slice header (ptr, len e cap), como o append realoca memória e por que maps não são thread-safe.

Publicada em · 5 min de leitura

Tamanho da fonte:
Largura da coluna:

Anatomia de Slices, Arrays e Maps em Go: Estruturas Internas e Armadilhas

As estruturas de dados primárias de coleções em Go — Arrays, Slices e Maps — são otimizadas para alto desempenho, alocação contígua de memória cache e baixa sobrecarga.

No entanto, o fato de o Go esconder ponteiros sob a sintaxe de slices e maps frequentemente induz desenvolvedores a cometerem erros sutis: sobrescrita silenciosa de dados em sub-slices, vazamento de blocos massivos de memória no Heap e pânicos fatais em operações concorrentes.


1. Arrays vs Slices: O Fim da Confusão

Em Go, Arrays e Slices são tipos conceitualmente e operacionalmente distintos:

  • Array: É um bloco contíguo de memória com tamanho fixo estático definido em tempo de compilação. O tamanho faz parte da assinatura do tipo: [5]int e [10]int são tipos incompatíveis. Além disso, arrays são valores puros: atribuir um array para outra variável ou passá-lo para uma função cria uma cópia integral de todos os seus elementos na memória.
  • Slice: É uma estrutura dinâmica e flexível de visualização (view) que referencia um Array subjacente (backing array).

2. A Anatomia do Slice Header

Internamente no runtime do Go, um slice não contém os dados diretamente. Ele é uma struct levíssima de 24 bytes (em arquiteturas de 64-bit), chamada Slice Header:

Anatomia Interna do Slice Header em Go
figura 1 · Anatomia Interna do Slice Header em Go
// Representação interna equivalente no runtime do Go
type SliceHeader struct {
    Data uintptr // 8 bytes: Ponteiro para o primeiro elemento no backing array
    Len  int     // 8 bytes: Quantidade de elementos atualmente presentes
    Cap  int     // 8 bytes: Capacidade máxima antes de exigir realocação
}
  • len(s): O número de elementos visíveis e acessíveis pelo slice.
  • cap(s): A distância entre o primeiro elemento do slice e o final do backing array contíguo.

3. A Mecânica do append() e a Estratégia de Crescimento

A função nativa append(slice, elem) possui dois comportamentos completamente distintos dependendo da capacidade disponível:

Mecânica do append: No Limite vs Nova Alocação
figura 2 · Mecânica do append: No Limite vs Nova Alocação

Cenário 1: len < cap (Escrita in-place rápida)

Se ainda houver espaço no backing array atual, o Go simplesmente incrementa o Len no header e grava o novo elemento diretamente na memória contígua já alocada. Custo: zero alocações de memória.

Cenário 2: len == cap (Estouro de capacidade)

Quando o backing array atinge sua capacidade máxima, o Go:

  1. Aloca um NOVO backing array maior no Heap;
  2. Copia todos os elementos do array antigo para o novo array (memcpy);
  3. Insere o novo elemento;
  4. Atualiza o ponteiro Data e o Cap no Slice Header retornado.

4. A Armadilha do Reslicing e a Sintaxe de 3 Índices

Quando criamos um sub-slice a partir de um slice pai (child := parent[1:3]), o Go não copia os dados: o filho aponta para o mesmo backing array do pai!

O Perigo da Sobrescrita:

parent := []int{10, 20, 30, 40, 50}
child := parent[1:3] // [20, 30], len=2, mas cap=4!

// Se chamarmos append em child:
child = append(child, 999) 
// 💥 PERIGO: Como cap=4, o append sobrescreveu in-place o valor 40 no array parent!
// parent agora é: [10, 20, 30, 999, 50]

A Solução: Full Slice Expression [low:high:max]

Para impedir que um sub-slice sobrescreva dados do pai, limite sua capacidade explicitamente com o 3º índice:

// [inicio : fim : capacidade_maxima]
child := parent[1:3:3] // len=2, cap=2!

// Agora, qualquer append em child é forçado a alocar um novo array,
// mantendo parent[3] intacto!
child = append(child, 999)

5. Vazamento de Memória por Sub-Slices (Memory Leaks)

Se você ler um arquivo ou payload de 100 Megabytes e fatiar apenas os primeiros 10 bytes:

func GetHeader(hugePayload []byte) []byte {
    return hugePayload[:10] // ❌ O SliceHeader de 10 bytes segura os 100MB inteiros no Heap!
}

Enquanto essa variável de 10 bytes estiver viva na memória, o Garbage Collector não consegue liberar os 100MB subjacentes, porque o ponteiro Data do slice ainda referencia o início do bloco.

A Solução com copy():

func GetHeaderSafe(hugePayload []byte) []byte {
    header := make([]byte, 10)
    copy(header, hugePayload[:10]) // Copia estritamente os 10 bytes para um novo array
    return header // Os 100MB originais agora podem ser coletados pelo GC!
}

6. A Anatomia Interna do Map (hmap)

Em Go, um Map é uma tabela hash (Hash Table) implementada sob a struct runtime.hmap.

Características Internas:

  1. Buckets: O map organiza seus dados em baldes (buckets). Cada bucket armazena até 8 pares de chave-valor.
  2. Iteração Não-Determinística: A ordem em que os itens são retornados em um for key, val := range meuMap é propositalmente aleatória. A cada loop, o runtime sorteia um bucket inicial aleatório para impedir que desenvolvedores escrevam códigos que dependam acidentalmente de ordem em tabelas hash.
  3. Pré-Alocação com hint:
    // ❌ Ruim: o map sofre re-hashing constante à medida que cresce
    m := make(map[string]User)
    
    // ✅ Excelente: pré-aloca buckets suficientes para 10.000 itens sem re-hashing
    m := make(map[string]User, 10000)

Maps NÃO são Concorrentes!

Se duas goroutines lerem e escreverem no mesmo map simultaneamente sem sincronização com sync.RWMutex, o runtime do Go aborta o programa imediatamente com um pânico que não pode ser capturado nem por recover(): fatal error: concurrent map read and map write.


7. Resumo Conceitual

  • Arrays têm tamanho fixo e são copiados por valor.
  • Slices são cabeçalhos de 24 bytes (Data, Len, Cap) que apontam para um array contíguo subjacente.
  • append() reutiliza a memória se houver capacidade; se lotado, aloca um novo array e copia os dados.
  • Use a sintaxe de 3 índices slice[low:high:max] para impedir que sub-slices corrompam o array pai.
  • Evite reter arrays gigantes no Heap utilizando copy() para extrair apenas fatias necessárias.
  • Maps são tabelas hash baseadas em buckets de 8 itens; use sempre o idioma coma-ok (val, ok := m[key]) e pré-aloque com make(map[K]V, hint) para máxima performance.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?