Modelo de Memória do Go: Pilha, Heap e Semântica de Valor
Go é uma linguagem com coleta de lixo e semântica de valor por padrão. Variáveis vivem em registros de ativação que o compilador pode colocar na pilha, no heap ou em armazenamento estático - mas você raramente escolhe onde. Em vez disso, você escolhe tipos: valores, ponteiros, slices, maps e interfaces. Esses tipos determinam o comportamento de cópia, o aliasing e o que o runtime deve rastrear para o coletor de lixo.
Esta página é a âncora conceitual para a seção Tipos de Dados e Memória. Fundamentos de Tipos de Dados mostra a sintaxe do dia a dia; os artigos sobre slices, maps, interfaces e channels explicam as representações do runtime. Aqui você aprende por que Go parece "passagem por valor", o que "tipo de referência" realmente significa em Go e como isso difere do modelo de memória de concorrência separado do Go (regras de happens-before para goroutines).
Variáveis Go são valores, a menos que você use um ponteiro; o compilador coloca o armazenamento na pilha ou no heap via análise de escape, e o GC recupera objetos de heap inacessíveis.
Insight: Bugs de aliasing, cópias inesperadas e gargalos de alocação geralmente vêm do mal-entendido da semântica de valor vs. ponteiro - não de memorizar frames da pilha.
Conceitos-Chave:semântica de valor, ponteiro, análise de escape, cabeçalho de slice/map/channel, valor zero, happens-before (concorrência).
Quando Usar: Use ponteiros quando uma função deve mutar o estado do chamador ou quando um struct grande não deve ser copiado a cada chamada; prefira valores quando a imutabilidade e a clareza prevalecem.
Limitações/Trade-offs: Você não pode forçar a alocação na pilha no código-fonte; a análise de escape pode surpreendê-lo após um refator; ponteiros e cabeçalhos compartilhados aumentam o risco de aliasing e race conditions.
Tópicos Relacionados: cabeçalhos de slice, buckets de map, itables de interface, ferramentas de análise de escape, detector de race.
Pense em uma variável Go como uma caixa nomeada que contém um valor de um determinado tipo. A atribuição copia os bits que constituem o valor (para a maioria dos tipos). Chamar uma função passa uma cópia do argumento, a menos que o tipo do parâmetro seja um ponteiro, caso em que a cópia é o valor do ponteiro (um endereço), não uma cópia profunda do struct apontado.
Go não expõe malloc/free ou transferência de propriedade no estilo Rust. A vida útil da memória é limitada pelo escopo mais a alcançabilidade: quando o último ponteiro para um objeto de heap desaparece, o GC o coleta. Isso é um contrato diferente de "este valor foi movido e a ligação antiga é inválida".
Pilha (stack) e heap são locais de implementação, não palavras-chave da linguagem. Variáveis locais que não escapam de uma função geralmente vivem na pilha da goroutine. Valores cujos endereços sobrevivem ao frame (ponteiros retornados, closures capturando locais, armazenando ponteiros em globais) são movidos para o heap. Você não anota isso; go build -gcflags=-m relata o que o compilador decidiu.
Vários tipos embutidos são semelhantes a referência, mas ainda assim passados por valor: um slice é um pequeno cabeçalho (ponteiro, comprimento, capacidade) copiado na atribuição; maps e channels são descritores que apontam para estruturas do runtime. Copiar o cabeçalho duplica o manipulador, não os dados de apoio - duas variáveis podem observar o mesmo array ou tabela hash.
A análise de escape é executada por função durante a compilação. Se o compilador não puder provar que o endereço de um valor não vaza, ele escapa para o heap. Gatilhos comuns de escape: retornar &local, atribuir &local a uma interface{}, anexar &local a um slice de ponteiros, ou capturar uma variável em uma goroutine que pode sobreviver ao frame.
frame do chamador frame do chamado
┌─────────────┐ ┌─────────────┐
│ s := []int │ cópia hdr │ fn(s []int) │
│ {1,2,3} │ ─────────► │ usa cabeçalho │
└──────┬──────┘ └─────────────┘
│
▼
array de apoio (heap ou pilha - o compilador decide)
A semântica de valor brilha no design de API: time.Time, structs pequenos e IDs são copiados de forma barata e reduzem a mutação oculta. A semântica de ponteiro troca a cópia por estado compartilhado: *Config mutado em um pacote afeta todos os detentores desse ponteiro. Slices ficam no meio: o cabeçalho é copiado, o array de apoio é compartilhado até que append realoque.
O modelo de memória de concorrência do Go (documentado em go.dev/ref/mem) é ortogonal à pilha/heap. Ele define quais leituras e escritas são permitidas entre goroutines através de sincronização: operações de canal, sync.Mutex, sync/atomic e sync.Once estabelecem arestas happens-before. Uma race condition é duas goroutines acessando a mesma memória, com pelo menos uma escrita, sem sincronização. O detector de race as encontra em tempo de execução; a análise de escape não.
Abordagem
Força
Fraqueza
Melhor Ajuste
Passar struct por valor
Propriedade clara, sem nil, menos races
Custo de cópia para structs grandes
Registros imutáveis pequenos, enums, coordenadas
Passar *T
Instância compartilhada única, mutação in-place
Verificações de nil, aliasing, risco de race
Configurações grandes, builders, parsers com estado
Micro-otimizar "manter na pilha" sem profiling geralmente é um esforço desperdiçado. O Go moderno inlines objetos pequenos e o GC Green Tea (padrão em Go 1.26) reduz a sensibilidade a pausas para muitas cargas de trabalho. Quando perfis de alocação mostram caminhos quentes, as correções são frequentemente: reutilizar buffers com sync.Pool, pré-alocar slices (make([]T, 0, n)), passar ponteiros apenas onde necessário ou reduzir o boxing para interface{}.
Valores zero são importantes: slices nil e maps nil se comportam de forma diferente (escrever em um map nil causa pânico; anexar a um slice nil funciona). Interfaces que contêm ponteiros nil tipados não são iguais a uma interface nil - um bug de produção comum coberto em Interface Nil vs Ponteiro Nil.
Para serviços, a semântica de valor nas fronteiras de domínio (retornar cópias, DTOs imutáveis) mais ponteiros apenas dentro de pacotes de alto desempenho é um padrão duradouro. Combine isso com o detector de race em CI e disciplina de mutex/canal de Fundamentos de Concorrência.
"Go passa argumentos de função por referência porque slices são referências" - Parâmetros são sempre passados por valor; valores de slice/map/channel são pequenos cabeçalhos cuja cópia ainda compartilha o armazenamento de apoio.
"Posso colocar structs grandes na pilha com uma palavra-chave" - Apenas a análise de escape decide; new(T) e &T{} ainda podem ser alocados na pilha se não escaparem.
"Ponteiros sempre significam heap" - Ponteiros que não escapam podem apontar para memória da pilha; heap vs. pilha é sobre tempo de vida, não sintaxe *.
"O documento do modelo de memória é sobre pilha e heap" - go.dev/ref/mem é sobre visibilidade de goroutine (happens-before), não sobre posicionamento de alocação.
"Copiar um slice copia os dados" - copy(dst, src) ou append para um novo slice podem copiar elementos; a atribuição apenas copia o cabeçalho.
O Go passa argumentos de função por referência ou por valor?
Sempre por valor. Se o parâmetro for *T, o valor copiado é o ponteiro (endereço), não o struct em si. Mutações através desse ponteiro são visíveis para todo o código que compartilha o mesmo endereço.
O que é análise de escape em uma frase?
A análise do compilador que decide se o armazenamento de uma variável deve sobreviver à sua função declarante e, portanto, deve ser alocado no heap.
Quando devo usar um receptor de ponteiro em vez de um receptor de valor?
Use receptores de ponteiro quando os métodos mutam o estado, quando o struct é grande o suficiente para que a cópia prejudique o desempenho, ou quando a consistência exige todos os métodos em *T. Receptores de valor são adequados para tipos imutáveis pequenos e evitam mutação acidental.
Maps são passados por referência?
Variáveis de map são descritores passados por valor. A atribuição ou passagem para uma função copia o descritor, não a tabela de buckets. Ambas as cópias se referem aos mesmos dados do map.
Qual é o valor zero de um ponteiro?
nil. Desreferenciar sem uma verificação de nil causa pânico. O valor zero de *int é nil, não um ponteiro para zero.
Como o modelo de memória de concorrência do Go se relaciona com a pilha e o heap?
São tópicos separados. O posicionamento da alocação é tempo de compilação + GC; o modelo de memória governa quais leituras/escritas entre goroutines são definidas quando você usa primitivas de sincronização ou canais.
Por que retornar um ponteiro para uma variável local funciona?
Se o ponteiro escapar da função, o compilador aloca o local no heap para que ele permaneça válido após o retorno. Se ele não escapar, o ponteiro pode se referir ao armazenamento da pilha que só é válido enquanto o frame estiver ativo.
Interfaces copiam dados?
Atribuir um valor de interface copia o par de palavras da interface (tipo + ponteiro de dados). Valores pequenos podem viver inline na palavra de dados; valores maiores vivem em outro lugar e a interface aponta para eles.
Quais ferramentas mostram decisões de pilha vs. heap?
go build -gcflags=-m registra as decisões de escape. Perfis de CPU e alocação (pprof) mostram onde o custo do runtime realmente se encontra - use-os antes de reescrever tipos.
A alocação na pilha é mais rápida que no heap?
Frequentemente sim para objetos minúsculos que não escapam, pois a alocação no heap atinge o alocador e a contabilidade do GC. A diferença varia por versão do Go e carga de trabalho; meça em vez de assumir.
Duas goroutines podem compartilhar uma pilha?
Não. Cada goroutine tem sua própria pilha (crescida conforme necessário). O compartilhamento ocorre através de objetos de heap, globais ou acesso sincronizado à memória que ambos referenciam.
O que acontece quando copio um struct contendo um campo slice?
A cópia do struct duplica cada campo. O cabeçalho do campo slice é copiado, então ambos os structs veem o mesmo array de apoio até que um lado anexe além da capacidade e realoque.
Versões de Pilha: Esta página foi escrita para Go 1.26.x (GC Green Tea padrão, modernizadores go fix - verificar patch na compilação), chi (última versão - verificar na compilação), gin (última versão - verificar na compilação), echo (última versão - verificar na compilação), google.golang.org/grpc (última versão - verificar na compilação), sigs.k8s.io/controller-runtime (última versão - verificar na compilação), kubebuilder (última versão - verificar na compilação), tinygo (última versão - verificar alvos de placa na compilação), wazero (última versão - verificar na compilação) e golangci-lint (última versão - verificar conjunto de linters na compilação).
cGJxcnRodnFyZi52YnxwdHZiNDEzfDIwMjYwNw==
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026