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
trocaacima). - O struct é grande e você quer evitar copiar dezenas de campos a cada chamada.
- Quer expressar “ausência” — um campo
*stringou*intpode sernil, enquanto umstringvazio é 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. *structcom campo nil no meio:a.b.cpânica sea.bfornil. Inicialize a cadeia ou valide por partes.- Confundir slice vazio com slice nil: ambos têm
len == 0, masjson.Marshalproduz[]enullrespectivamente. Para APIs, seja explícito commake([]T, 0). - Retornar ponteiro para mutex:
*sync.Mutexcompartilhado entre goroutines pode ser correto, mas geralmente é melhor embutir o valor e passar o ponteiro do struct inteiro.
Checklist rápido
- Pass-by-value sempre; ponteiro é a forma de mutar o original.
- Slice, map, channel já são referência — não enrole com
*slice. - Pointer receiver para mutar ou struct grande; seja consistente no tipo.
- Cheque
nilantes de dereferenciar quando o ponteiro vem de fora. - Nunca compartilhe ponteiro mutável entre goroutines sem sincronização.
- 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.