Na maior parte do tempo em Go, os valores fluem pelo programa sendo copiados — você passa um int para uma função, a função recebe sua própria cópia, e o original permanece intocado. Essa simplicidade é intencional. Mas há situações em que copiar não é o que você quer: uma função precisa modificar a variável do chamador, sinalizar que um valor está ausente, ou evitar duplicar um grande bloco de memória. Os pointers resolvem esses três problemas. Entender quando e por que utilizá-los é uma das decisões mais importantes ao programar em Go.
Mutabilidade
Antes de apresentar os pointers, vale entender como Go trata a mutabilidade. Toda variável em Go é mutable ou immutable, e a forma de declaração determina qual das duas você obtém.
Uma variável mutable pode ter seu valor alterado a qualquer momento após ser declarada. Dentro de uma função, uma declaração com var (ou a declaração curta com :=) cria uma variável mutable:
Uma variável immutable, declarada com const, não pode ser alterada após ser definida. O compilador aplica essa restrição em tempo de compilação:
const é para valores fixados por design — constantes matemáticas, limites de configuração, valores enumerados. var (ou :=) é para tudo que precisa mudar ao longo do tempo.
A distinção entre mutable e immutable importa porque ela molda como você raciocina sobre um programa. Quando um valor não pode mudar, você não precisa rastrear quem poderia modificá-lo. Quando pode, você precisa — e é exatamente aí que os pointers entram em cena.
Mutabilidade e pointers
As funções em Go recebem argumentos como cópias. Se você quer que uma função modifique uma variável declarada no chamador, não é possível passar o valor em si — a função estaria modificando sua própria cópia e o chamador não veria nenhuma alteração. Em vez disso, você passa um pointer: o endereço de memória da variável.
O operador & obtém o endereço de uma variável. O operador *, colocado à frente de um pointer, o dereferencia — ele segue o endereço e retorna o valor armazenado ali. Um tipo escrito como *T é um pointer para um valor do tipo T.
increment recebe o endereço de count. Dentro da função, *n lê o valor atual naquele endereço, soma um e escreve o resultado de volta. Quando main imprime count, a alteração é visível porque tanto main quanto increment referenciam a mesma posição de memória.
Esse é o caso de uso fundamental para pointers em Go: dar a uma função a capacidade de modificar uma variável que existe em outro lugar.
Indicando a ausência de um valor
Ocasionalmente, uma função precisa comunicar não apenas "aqui está o resultado", mas também "não há resultado". Um zero value nem sempre é suficiente para isso — se uma função retorna 0 para um int, o chamador não consegue distinguir se a resposta foi genuinamente zero ou se nenhuma resposta existe.
Os pointers resolvem isso de forma elegante porque um pointer pode ser nil. Quando uma função retorna *int em vez de int, ela pode retornar nil para sinalizar ausência e um pointer não-nulo para sinalizar presença:
O chamador verifica se o pointer é nil antes de dereferenciar. Esse padrão aparece em toda a biblioteca padrão do Go e na maioria do código Go real. É especialmente comum em funções que retornam tanto um valor quanto um error, onde um pointer nil no lado do valor deixa explícito que nenhum resultado significativo foi produzido.
Performance e structs grandes
Toda vez que você passa um valor para uma função, Go o copia. Para tipos pequenos — integers, booleans, structs com poucos campos — o custo dessa cópia é insignificante. Mas para structs grandes com muitos campos, a cópia pode se tornar mensurável, especialmente se a função for chamada com frequência em um loop apertado.
Passar um pointer em vez do valor evita a cópia: a função recebe um único endereço de memória (tipicamente 8 bytes em sistemas 64-bit), independentemente do tamanho da struct apontada.
Dito isso, a orientação em Go é conservadora: prefira passar valores por padrão. Semântica de pointer introduz aliasing — múltiplas partes do programa mantendo uma referência para a mesma memória — o que torna o código mais difícil de raciocinar e testar. Recorra a um pointer somente quando você mediu ou tem forte razão para esperar um problema de performance, ou quando a struct é grande o suficiente para que o custo seja evidente.
Go tem garbage collection
Quando você obtém o endereço de uma variável local, está dizendo ao runtime que esse valor pode sobreviver à função que o criou. Go responde alocando a variável na heap em vez da stack. O garbage collector é responsável por recuperar essa memória assim que não existirem mais pointers apontando para ela.
Essa é uma das funcionalidades mais importantes do Go em termos de qualidade de vida: você não precisa liberar memória heap manualmente. Não há free(), nenhum destrutor, nenhum reference counting para gerenciar. O garbage collector cuida disso.
Mesmo que n seja uma variável local dentro de newCounter, retornar &n é seguro. A análise de escape do Go detecta que n escapa da função e o coloca na heap automaticamente.
Juntando tudo
Os pointers em Go servem a três propósitos distintos, e cada um tem um caso de uso natural:
| Propósito | Quando utilizá-lo |
|---|---|
| Mutabilidade | Uma função precisa modificar uma variável no chamador |
| Ausência de valor | Uma função precisa sinalizar "nenhum resultado" via nil |
| Performance | Uma struct é grande o suficiente para que copiá-la seja mensurável |
Fora esses três cenários, passar valores é quase sempre o padrão correto. Valores são mais simples de raciocinar, mais fáceis de testar e não exigem verificações de nil. Deixe a necessidade surgir antes de adicionar indireção via pointer — e quando ela surgir, documente o que sua função faz com a memória que recebe.