← Voltar para o blog

Ponteiros em Go: Entenda de Vez Valor, Referência e Métodos

Ponteiros em Go explicados sem mistério: & e *, valor vs referência, quando usar em structs e métodos, nil, goroutines, interfaces e as armadilhas mais comuns — com exemplos.

Ponteiros é o primeiro assunto que trava quem chega em Go vindo do Python, JavaScript ou Java. Nas linguagens gerenciadas, a memória “simplesmente funciona”: você cria um objeto, passa ele pra frente e ninguém fala de endereço. Em Go, de repente aparecem &, *, *Usuario, func (u *Usuario) e a sensação é a de que voltou pra faculdade de C. A boa notícia: ponteiro em Go é muito mais simples e seguro que em C. Não há aritmética de ponteiros, não há malloc/free e o coletor de lixo cuida da memória. O que resta é um conceito pequeno que resolve três problemas práticos: mutar um valor, evitar cópia desnecessária e expressar “isto pode estar ausente”.

Este guia cobre o suficiente para você parar de adivinhar entre func (u Usuario) e func (u *Usuario), entender por que sua função não altera o slice da forma como esperava, e evitar os dois bugs clássicos de quem está começando. Se você ainda está destrinchando a base da linguagem, leia antes o guia de Go para iniciantes e a introdução a interfaces; ponteiros fazem mais sentido depois que structs e métodos estão sólidos.

O que é um ponteiro, sem mistério

Um ponteiro é o endereço de uma variável na memória. Em vez de guardar o valor 42, você guarda “onde está o 42”. Dois operadores resolvem 90% do uso:

x := 42
p := &x        // p é *int (ponteiro para int) e vale o endereço de x
fmt.Println(p) // 0xc0000b2000 (um endereço, não 42)
fmt.Println(*p) // 42 (lê o valor apontado)
*p = 100       // escreve no endereço — agora x vale 100
fmt.Println(x)  // 100

& (“endereço de”) e * (“valor em”) são as duas faces da mesma moeda. &x pega o endereço; *p dereferencia (lê ou escreve o que está lá). O tipo *int se lê “ponteiro para int”. O zero value de qualquer ponteiro é nil — um ponteiro que não aponta pra lugar nenhum.

Valor vs referência: o exemplo que fixa

Go é sempre pass-by-value. Cada chamada de função copia os argumentos. Isso significa que mutar um parâmetro dentro da função não afeta quem chamou — a menos que você passe um endereço. Veja o exemplo clássico:

package main

import "fmt"

// Não funciona: a, b e c são cópias locais.
func trocaErrado(a, b int) {
    a, b = b, a
}

// Funciona: a e b apontam para as variáveis do chamador.
func troca(a, b *int) {
    *a, *b = *b, *a
}

func main() {
    x, y := 1, 2
    trocaErrado(x, y)
    fmt.Println(x, y) // 1 2 — nada mudou

    troca(&x, &y)
    fmt.Println(x, y) // 2 1 — agora sim
}

trocaErrado recebe cópias de x e y; a troca acontece só dentro da função e some no return. troca recebe os endereços, então escrever em *a modifica a variável original. Esse é o motivo de existirem ponteiros: dar à função uma forma de alcançar o dado que está fora dela.

A regra que mais engana: slices, maps e channels já são referências

Quem vem de outra linguagem tenta “otimizar” passando *[]int ou *map[string]int — e está errado. Em Go, slices, maps e channels já carregam ponteiros internamente. Passar o valor deles (não o ponteiro) já permite que a função muta o conteúdo:

func appendTag(s []string) []string {
    return append(s, "go")
}

func setar(m map[string]int) {
    m["brasil"] = 1 // muta o map original — não precisa de *map
}

func main() {
    m := map[string]int{}
    setar(m)
    fmt.Println(m["brasil"]) // 1
}

A armadilha com slice é sutil: append pode (ou não) realocar o array interno. Se você fizer append(s, x) sem retornar, o chamador pode não ver o novo elemento. Por isso a assinatura idiomática é func appendTag(s []string) []string — devolva o slice. Não passe *[]string; isso é quase sempre erro de entendimento. Para fixar os tipos compostos de uma vez, o cheatsheet de slices, maps e structs é um bom atalho.

Quando usar ponteiros (e quando não)

Use ponteiros quando:

  • Precisa mutar o original (o caso do troca acima).
  • O struct é grande e você quer evitar copiar dezenas de campos a cada chamada.
  • Quer expressar “ausência” — um campo *string ou *int pode ser nil, enquanto um string vazio é ambíguo entre “não informado” e “vazio”.
  • Implementa um método que altera o receiver, como func (c *Conta) Sacar(valor int).

Não use ponteiros quando:

  • O tipo já é referência (slice, map, channel, function, interface).
  • O struct é pequeno (até uns 5–8 campos primitivos). Copiar é barato, fica na cache, e evita pressão no garbage collector.
  • A função só lê o valor. Receba por valor — fica claro que não há efeito colateral e o código fica mais fácil de raciocinar.
  • Você quer otimizar sem medir. Ponteiros às vezes pioram a performance porque forçam alocação no heap (mais sobre isso adiante).

O instinto saudável em Go é o oposto do de C: comece sem ponteiro e adicione só quando aparece um motivo real. Código cheio de * sem necessidade é mais difícil de ler e mais lento.

Structs e métodos: value receiver vs pointer receiver

A decisão func (u Usuario) vs func (u *Usuario) aparece em todo projeto. A regra prática:

type Usuario struct {
    Nome  string
    Email string
}

// Value receiver: não muta, Usuario é pequeno, cópia é barata.
func (u Usuario) Saudacao() string {
    return "Olá, " + u.Nome
}

// Pointer receiver: muta o receiver original.
func (u *Usuario) DefinirEmail(email string) {
    u.Email = email
}

Use pointer receiver quando o método muta o receiver, quando o struct é grande, ou para manter consistência com outros métodos do tipo que já usam ponteiro (misturar os dois no mesmo tipo é fonte de confusão e de métodos “somehow” não aparecendo na interface). Use value receiver para structs pequenos e imutáveis, como time.Time. O guia de dependency injection sem framework mostra como esse padrão sustenta código testável: você depende de interfaces pequenas e os receivers concretos são injetados.

nil, ponteiros e o pânico

Dereferenciar um ponteiro nil causa pânico em runtime — é uma das fontes mais comuns de nil pointer dereference em produção:

var u *Usuario      // u é nil
fmt.Println(u.Nome) // panic: runtime error: invalid memory address

A defesa é sempre checar antes de usar quando o ponteiro pode ser nil (vindo de um mapa, de um JSON, de uma função que “talvez” retorne):

if u != nil {
    fmt.Println(u.Nome)
}

Atenção à diferença entre tipos: um map nil permite leitura (devolve o zero value) mas pânico ao escrever; um slice nil funciona com len, append e range. Ponteiros nil pânicam em qualquer acesso. Para os erros típicos de runtime, a seção de erros comuns em Go tem páginas dedicadas ao nil pointer dereference e às condições de corrida.

new(T), &T{} e o mito do “perigoso como C”

Duas formas de obter um ponteiro:

p1 := new(int)    // *int, valor 0
*p1 = 42

u := &Usuario{Nome: "Ana"} // *Usuario, idiomático

new(T) devolve *T apontando para um T zerado; raramente aparece em código real. &T{...} (composite literal com &) é a forma idiomática quando você quer inicializar já com valores. Diferente de C, retornar o endereço de uma variável local é totalmente seguro em Go:

func novoContador() *int {
    c := 0
    return &c // seguro: escape analysis move c para o heap
}

O compilador detecta (via escape analysis) que &c escapa da função e aloca c no heap; o coletor de lixo libera quando ninguém mais usa. Não há dangling pointer.

Ponteiros e interfaces: o nil trap clássico

A interface em Go é um par (tipo, valor). Uma interface só é nil quando ambos são nil. Devolver um ponteiro tipado nil dentro de uma interface produz uma interface não-nil que vale nil — bug sutil:

type Repositorio interface{ Buscar(id int) *Usuario }

func (db *DB) Buscar(id int) *Usuario {
    return nil // *Usuario nil
}

var r Repositorio = &DB{}
u := r.Buscar(1)
if u == nil { // true aqui — ok, porque u é *Usuario
}

// Mas isto quebra:
var err error
err = retornaErroConcreto() // retorna (*MeuErro)(nil)
if err != nil {             // true! a interface não é nil
    // executa mesmo sem erro de verdade
}

A regra: devolva nil concreto do tipo retornado e deixe quem chama atribuir à interface, ou retorne a interface direto como nil. O guia de interfaces em Go aprofunda esse comportamento, que é cobrado em perguntas de entrevista.

Ponteiros entre goroutines: não compartilhe, comunique

Compartilhar um *T entre goroutines e ler/escrever sem sincronização é data race — comportamento indefinido, bugs que só aparecem sob carga:

// DATA RACE: duas goroutines escrevendo no mesmo *int
func main() {
    n := 0
    go func() { n++ }()
    go func() { n++ }()
}

Rode sempre com go test -race ou go run -race para capturar isso. A filosofia do Go é “don’t communicate by sharing memory; share memory by communicating” — passe o dado por um channel ou proteja com sync.Mutex/sync/atomic. Os padrões de concorrência e o guia de errgroup mostram como orquestrar goroutines com cancelamento e limite sem cair em race. Quem chega em entrevistas técnicas de Go sem saber responder “por que não compartilhar ponteiros entre goroutines” perde pontos fáceis.

Ponteiros e performance: quando copiar é mais barato

Surpresa comum: adicionar ponteiros “para evitar cópia” piorou a performance. Motivo: quando o compilador vê que um ponteiro escapa da função, aloca no heap, e cada heap allocation vira trabalho para o garbage collector. Uma struct pequena passada por valor fica na stack — zero GC.

A regra é medir, não adivinhar. Escreva um benchmark, rode com -benchmem, e só converta para ponteiro se a cópia realmente aparecer como gargalo. Para entrar nesse tipo de análise, o guia de pprof em produção e o de sync.Pool para reutilização de objetos são os próximos passos naturais — e o Effective Go em 2026 resume o que da filosofia original ainda orienta essas decisões.

Armadilhas comuns para revisar antes do commit

  • Ponteiro para variável de loop (pré-Go 1.22): for _, v := range xs { save(&v) } salvava o mesmo endereço a cada iteração. A partir do Go 1.22, v é uma nova variável por iteração — o bug sumiu, mas vale saber se você mantém código antigo.
  • *struct com campo nil no meio: a.b.c pânica se a.b for nil. Inicialize a cadeia ou valide por partes.
  • Confundir slice vazio com slice nil: ambos têm len == 0, mas json.Marshal produz [] e null respectivamente. Para APIs, seja explícito com make([]T, 0).
  • Retornar ponteiro para mutex: *sync.Mutex compartilhado entre goroutines pode ser correto, mas geralmente é melhor embutir o valor e passar o ponteiro do struct inteiro.

Checklist rápido

  1. Pass-by-value sempre; ponteiro é a forma de mutar o original.
  2. Slice, map, channel já são referência — não enrole com *slice.
  3. Pointer receiver para mutar ou struct grande; seja consistente no tipo.
  4. Cheque nil antes de dereferenciar quando o ponteiro vem de fora.
  5. Nunca compartilhe ponteiro mutável entre goroutines sem sincronização.
  6. Meça antes de “otimizar” com ponteiros; cópia na stack costuma ganhar.

Ponteiros em Go são menos do que parecem: dois operadores (& e *), uma decisão de receiver por tipo e disciplina com nil e concorrência. Quem internaliza isso para de brigar com a linguagem e passa a escrever código idiomático — o que é exatamente o que times Go no Brasil procuram. Para fechar o ciclo de fundamentos, combine este guia com o de erros em Go, o de interfaces e a trilha completa no hub de tutoriais. Para quem está de olho no mercado, as vagas de Go no Brasil e o comparativo de salários de desenvolvedor Go mostram onde esses fundamentos viram oportunidade — e o portal eu.dev.br reúne vagas de tecnologia no Brasil ajuda a comparar a demanda por Go com outras stacks lado a lado.