Pular para o conteúdo principal

Pointers

8 min de leitura•Arquivado emLinguagem de Programação Goem

Aprenda como os pointers funcionam em Go, quando usá-los para mutabilidade e performance, e como o garbage collector gerencia a memória heap.

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ósitoQuando utilizá-lo
MutabilidadeUma função precisa modificar uma variável no chamador
Ausência de valorUma função precisa sinalizar "nenhum resultado" via nil
PerformanceUma 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.