Bem-vindo ao Blog da DMarkInfo

Conteúdos e novidades sobre Tecnologia da Informação.

O Guia Prático de Ponteiros em Go: Quando Usar, Quando Evitar e Como Não Dar um Tiro no Pé

Postado por Eduardo Marques em 27/06/2026
O Guia Prático de Ponteiros em Go: Quando Usar, Quando Evitar e Como Não Dar um Tiro no Pé

Se você veio de linguagens como C ou C++, a palavra "ponteiro" provavelmente te traz lembranças de telas azuis, falhas de segmentação e uma caça incessante por vazamentos de memória. Por outro lado, se o seu background é JavaScript, Python ou Java, o conceito pode parecer um preciosismo desnecessário do pessoal de "baixo nível". Quando chegamos ao Go, nos deparamos com um meio-termo pragmático: temos ponteiros, mas não temos a perigosa aritmética de ponteiros que tornava tudo um caos nas linguagens mais antigas.

Em Go, tudo é passado por valor. Isso significa que, toda vez que você passa uma variável para uma função, o Go faz uma cópia idêntica daquele dado na memória. É aí que os ponteiros entram como heróis ou vilões do seu código. Em vez de copiar uma estrutura gigante, você pode simplesmente passar o endereço de memória onde ela está guardada. Parece a solução perfeita para economizar memória e dar velocidade ao sistema, mas, na prática do desenvolvimento de software, a realidade é um pouco mais sutil e cheia de trade-offs.

 

O que são e como funcionam de verdade sob o capô

Para entender um ponteiro, pense na memória do seu computador como um imenso armário cheio de gavetas numeradas. Cada gaveta guarda um valor — por exemplo, o número 42. Uma variável comum é o nome que você dá para essa gaveta. Um ponteiro, por sua vez, é uma gaveta especial que guarda o número de outra gaveta. Em Go, usamos o operador & (e comercial) para descobrir o número da gaveta (o endereço) e o operador * (asterisco) para "olhar dentro" da gaveta que o ponteiro está apontando.

Muitos desenvolvedores que estão começando com Go se confundem com a sintaxe do asterisco, porque ela assume dois papéis diferentes dependendo do contexto. Quando você declara um tipo, como *int, o asterisco significa "este tipo é um ponteiro para um inteiro". Quando você usa o asterisco antes de uma variável já existente, como *minhaVariavel, você está fazendo a "derreferenciação", que nada mais é do que ler ou modificar o valor real que está guardado naquele endereço.

Um detalhe crucial sobre o comportamento do Go é o conceito de escape analysis (análise de escape) feito pelo compilador. Em linguagens antigas, se você criasse uma variável dentro de uma função e tentasse retornar o ponteiro dela, você estaria retornando um endereço inválido, gerando um bug clássico. O Go resolve isso de forma brilhante: o compilador analisa o código e, se perceber que uma variável vai "escapar" do escopo da função, ele automaticamente a aloca na heap (memória global) em vez da stack (memória local rápida).

Isso tira um peso enorme das costas do desenvolvedor, mas introduz um custo oculto. Variáveis que escapam para a heap precisam ser limpas pelo Garbage Collector (GC) do Go mais tarde. Portanto, o uso indiscriminado de ponteiros, na tentativa ingênua de otimizar a cópia de dados, pode acabar sobrecarregando o GC e deixando sua aplicação backend mais lenta e com picos de latência que são difíceis de rastrear em produção.

 

Quando você DEVE usar ponteiros no seu código

O primeiro cenário onde os ponteiros são indispensáveis é quando você precisa modificar o estado de uma variável dentro de uma função ou método. Se você passar uma struct de configuração para uma função sem usar ponteiro, qualquer alteração feita nos campos dessa struct morrerá assim que a função terminar, pois você alterou apenas uma cópia local. Para métodos de structs, os chamados pointer receivers (func (s *Service) Update()) são o padrão da indústria sempre que o método precisa alterar dados internos da struct.

O segundo motivo clássico é o ganho de performance ao lidar com structs massivas. Imagine que sua aplicação está processando um payload JSON gigante que se transforma em uma struct com dezenas de campos, arrays internos e mapas. Passar essa struct de função em função vai gerar milhares de cópias desnecessárias na memória, destruindo o cache de CPU. Passar um ponteiro garante que apenas 8 bytes (em sistemas de 64 bits) sejam copiados, independentemente do tamanho real da estrutura de dados.

Por fim, os ponteiros são essenciais para representar a ausência de valor de forma explícita. Se você tem um campo em um banco de dados que pode ser nulo, como deleted_at, uma variável do tipo time.Time em Go assumirá o seu valor zero (0001-01-01) caso não preenchida. Se você transformar esse campo em *time.Time, o valor padrão quando não houver data será nil. Isso facilita integrações com APIs e ORMs, diferenciando claramente um campo vazio de um campo que nunca foi preenchido.

 

Quando os ponteiros viram seus inimigos: evite-os

Existe um mito no ecossistema Go de que usar ponteiros para tudo deixa o código mais rápido. Isso é mentira. Se você está lidando com tipos primitivos do Go, como int, float64, string ou structs muito pequenas (com dois ou três campos simples), passar por valor é quase sempre mais rápido do que passar um ponteiro. A cópia desses tipos pequenos é extremamente barata para a CPU e mantém os dados na stack, evitando o trabalho pesado do Garbage Collector.

Outro ponto que causa confusão são os tipos que já são ponteiros disfarçados por baixo dos panos. Slices, maps e channels em Go são estruturas de cabeçalho que contêm ponteiros para os dados reais. Você quase nunca deve criar um ponteiro para um slice (*[]string) ou ponteiro para um map (*map[string]int). Fazer isso adiciona uma camada de indireção totalmente inútil, tornando a sintaxe do seu código horrível e aumentando as chances de você estourar um erro de ponteiro nulo em tempo de execução.

A segurança e a previsibilidade do código também diminuem quando abusamos de ponteiros. Quando você passa um ponteiro para várias funções distintas no seu backend, você está compartilhando o mesmo estado mutável entre elas. Se a função C alterar um campo sem avisar, as funções A e B terão seus comportamentos afetados. Código com estado compartilhado e mutável é um terreno fértil para bugs bizarros, condições de corrida (race conditions) em ambientes concorrentes com goroutines e dores de cabeça terríveis para testar.

 

Conclusão: a filosofia do pragmatismo no backend

Dominar ponteiros em Go não é sobre decorar regras de sintaxe, mas sim sobre desenvolver uma intuição sobre como os dados fluem pela memória da sua aplicação. O design da linguagem foca em simplicidade e clareza, e o seu código deve refletir isso. Na dúvida entre usar ou não um ponteiro, comece passando o valor diretamente. Deixe que o perfilamento de performance (profiling) com a ferramenta pprof do Go te diga se aquela cópia de dados está realmente sendo um gargalo.

Escrever software robusto para o ecossistema de backend exige que sejamos previsíveis. É muito melhor ter um código legível, seguro contra concorrência e que trate os dados de forma imutável, do que tentar adivinhar o comportamento do compilador criando ponteiros para cada struct do projeto. Guarde os ponteiros para os momentos certos: quando a mutação de estado for estritamente necessária ou quando o tamanho do dado justificar o bypass da cópia.

No fim do dia, a beleza do Go está justamente nessa liberdade controlada. Não temos que gerenciar a memória manualmente como os engenheiros de sistemas complexos de antigamente, mas ainda temos as ferramentas certas nas mãos para garantir que nossos microsserviços aguentem milhões de requisições por segundo sem abrir bico. Use os ponteiros com respeito, entenda o impacto no Garbage Collector e seu backend rodará suave como seda em produção.

Compartilhe este post:
Voltar para a Home