Introdução: Por que "apenas um struct" não é tão simples assim?
\n\nVocê já escreveu em Go e, olhando para seu programa, se perguntou: "Onde minha estrutura está morando agora? Na pilha rápida e aconchegante ou nos vastos domínios do heap sob a supervisão do coletor de lixo?". Essa questão atormenta não apenas iniciantes. No Stack Overflow, o tópico "Stack vs heap allocation of structs in Go, and how they relate to garbage collection" acumulou milhares de visualizações. E não é por acaso.
\n\nA aparente simplicidade do Go esconde um mecanismo poderoso — o escape analysis. Este é um detetive do compilador que decide o destino de cada objeto. Sua decisão afeta o desempenho da sua aplicação e a carga no GC (Garbage Collector). Neste tutorial passo a passo, vamos colocar o chapéu de detetive e entender:
\n- \n
- Por que uma estrutura vive na pilha e outra no heap? \n
- Como ponteiros e closures quebram a paz da pilha. \n
- Como esse conhecimento ajuda você a escrever código rápido e eficiente em Go. \n
Pilha vs Heap: Breve introdução
\nAntes de mergulhar no código, vamos refrescar a teoria. Não se preocupe, será rápido e direto.
\n\nPilha (Stack)
\n- \n
- Velocidade: Relâmpago. Alocar memória é apenas mover o ponteiro para baixo. \n
- Ciclo de vida: Estritamente ligado à chamada da função. Vive enquanto a função está em execução. \n
- Gerenciamento: Automático. "Desaparece" ao sair da função. \n
- Tamanho: Pequeno e fixo por goroutine (geralmente 2-4 KB). \n
Heap
\n- \n
- Velocidade: Mais lento. Requer busca por um bloco de memória livre. \n
- Ciclo de vida: Enquanto houver referências ao objeto no programa. Pode sobreviver à função que o criou. \n
- Gerenciamento: Coletor de lixo (GC). Ele varre o heap, encontra objetos "mortos" e libera memória. Isso custa recursos de CPU. \n
- Tamanho: Grande (limitado apenas pela memória do SO). \n
A regra de ouro do Go: "Mantenha os dados na pilha o máximo possível". Isso reduz a carga no GC e acelera o programa.
\n\nEscape Analysis: O detetive chefe do Go
\nEm Go, você não escreve new ou malloc para indicar explicitamente onde colocar um objeto. O compilador decide isso por você. Esse processo é chamado de escape analysis (análise de escape).
Ele analisa seu código e faz uma pergunta: "A referência a este objeto pode sair da função atual?".
\n- \n
- Se NÃO (objeto não escapa) → O objeto é colocado na pilha. \n
- Se SIM (objeto escapa) → O objeto é colocado no heap. \n
Vamos ver isso em ação.
\n\nExemplo 1: Estrutura segura na pilha
\npackage main\n\nimport "fmt"\n\ntype Point struct {\n X, Y int\n}\n\n// createPoint cria um Point e o retorna por valor.\nfunc createPoint(x, y int) Point {\n p := Point{X: x, Y: y}\n return p // p é copiado no retorno\n}\n\nfunc main() {\n p1 := createPoint(10, 20)\n fmt.Println(p1)\n}\n\nNeste exemplo, p é uma variável local. Ela é criada dentro de createPoint e retornada por valor (copiada). A própria estrutura p não pode ser usada fora da função (sua cópia é usada em main). O escape analysis vê isso e, felizmente, coloca p na pilha. O GC descansa.
Exemplo 2: Escape através de ponteiro
\nAgora vamos mudar apenas uma linha — retornar um ponteiro.
\npackage main\n\nimport "fmt"\n\ntype Point struct {\n X, Y int\n}\n\n// createPointer cria um Point e retorna um ponteiro para ele.\nfunc createPointer(x, y int) *Point {\n p := P