Um ponteiro é uma variável que armazena o endereço de memória de outra variável — não o valor em si, mas onde ele está guardado. Em Go, ponteiros são explícitos e intencionais: você os obtém com & e os usa com *. Não há aritmética de ponteiros como no C, e o garbage collector cuida da liberação de memória.
Entender ponteiros em Go é entender por que o código funciona (ou não). Esta referência cobre tudo desde o básico até escape analysis e armadilhas clássicas.
Por que Go é pass-by-value — demonstração com structs grandes
Go passa cópias de tudo. Sem exceção. Quando você chama uma função com uma variável, o runtime copia o valor inteiro para os parâmetros da função. Para tipos primitivos (int, bool, float64), isso é trivial — 4 a 8 bytes. Para structs grandes, o custo pode ser significativo.
// Struct grande — 200+ bytes por cópia
type Product struct {
ID string // 16 bytes (header de string)
Name string // 16 bytes
Description string // 16 bytes
Tags [10]string // 160 bytes (array fixo de strings)
Price float64 // 8 bytes
Stock int // 8 bytes
// Total: ~224 bytes por cópia
}
// Passa por valor — copia 224 bytes a cada chamada
func describeByValue(p Product) string {
return fmt.Sprintf("%s (R$ %.2f)", p.Name, p.Price)
}
// Passa por ponteiro — copia apenas 8 bytes (endereço de memória)
func describeByPtr(p *Product) string {
return fmt.Sprintf("%s (R$ %.2f)", p.Name, p.Price)
}
// Benchmark mental: 1 milhão de chamadas
// describeByValue: 1M × 224 bytes = 224 MB copiados
// describeByPtr: 1M × 8 bytes = 8 MB copiados
func main() {
p := Product{Name: "Camiseta", Price: 59.90}
s1 := describeByValue(p) // cópia de p é criada na entrada
s2 := describeByPtr(&p) // apenas o endereço de p é copiado
fmt.Println(s1, s2)
}A implicação prática: funções que recebem structs grandes por valor e não precisam modificá-las devem usar ponteiro por questão de performance. Porém, para structs pequenas (menos de ~64 bytes com campos simples), a cópia é geralmente mais rápida do que a indireção via ponteiro, porque o CPU cache funciona melhor com dados copiados localmente.
Operadores & (address-of) e * (dereference) com diagrama conceitual
Dois operadores controlam ponteiros em Go. São simétricos: & vai do valor para o endereço; * vai do endereço para o valor.
Memória RAM:
Endereço Valor
0xc000018030 │ 42 │ ← variável x
└─────┘
↑
│ p := &x
┌─────┐
0xc0000b4010 │ 0xc000018030 │ ← variável p (um *int)
└─────────────┘
p → armazena o endereço 0xc000018030
*p → vai até 0xc000018030 e lê/escreve o valor láx := 42
fmt.Println(x) // 42 — o valor direto
p := &x // & captura o endereço de x
fmt.Println(p) // 0xc000018030 — o endereço (muda a cada execução)
fmt.Printf("%T\n", p) // *int — tipo de p é ponteiro para int
fmt.Println(*p) // 42 — dereference: lê o valor no endereço
*p = 100 // escreve via ponteiro
fmt.Println(x) // 100 — x foi modificado através de p
// Cadeia de ponteiros — ponteiro para ponteiro
pp := &p // pp é **int
fmt.Println(**pp) // 100 — dois deferences para chegar ao valor
// Operadores sobre campos de struct
type Point struct{ X, Y int }
pt := Point{X: 3, Y: 4}
pptr := &pt
// Dereference manual — verbose, raramente necessário
(*pptr).X = 10
// Auto-dereference — Go faz implicitamente
pptr.X = 10 // equivalente a (*pptr).X = 10
fmt.Println(pptr.X) // 10Pointer to struct — acesso automático sem dereference
Quando você tem um ponteiro para struct, Go aplica o dereference automaticamente ao acessar campos e chamar métodos. Isso torna o código com ponteiros quase indistinguível do código com valores — intencionalmente.
type Order struct {
ID string
CustomerID string
Status string
Total float64
Items []OrderItem
}
// &Order{} retorna *Order diretamente
order := &Order{
ID: "ord-42",
CustomerID: "cust-7",
Status: "pending",
Total: 299.90,
}
// Acesso a campos — auto-dereference
fmt.Println(order.Status) // "pending" — Go faz (*order).Status
order.Status = "confirmed" // Go faz (*order).Status = "confirmed"
// Encadeamento de campos com ponteiros aninhados
type Customer struct {
ID string
Address *Address
}
type Address struct {
City string
Country string
}
cust := &Customer{
ID: "c-1",
Address: &Address{City: "São Paulo", Country: "BR"},
}
city := cust.Address.City // Go resolve (*(*cust).Address).City automaticamente
// Comparação — ponteiros iguais se apontam para o mesmo endereço
a := &Order{ID: "1"}
b := a // b aponta para o mesmo Order
c := &Order{ID: "1"} // c aponta para um Order DIFERENTE com mesmo conteúdo
fmt.Println(a == b) // true — mesmo endereço
fmt.Println(a == c) // false — endereços diferentes
fmt.Println(*a == *c) // true — valores iguais (se Order não tiver campos não-comparáveis)nil pointer — verificação, panics e defensividade
O zero value de qualquer ponteiro é nil. Um ponteiro nil não aponta para nenhuma memória válida. Desreferenciar um ponteiro nil causa panic: runtime error: invalid memory address or nil pointer dereference — um dos erros mais comuns em Go para iniciantes.
// Zero value de ponteiro é nil
var p *Order
fmt.Println(p) // <nil>
fmt.Println(p == nil) // true
// Panic — nunca desreferencie sem verificar
// p.Status = "confirmed" // PANIC: nil pointer dereference
// _ = p.ID // PANIC
// Verificação correta
if p != nil {
fmt.Println(p.Status) // seguro
}
// Padrão: retornar nil como "ausência de valor"
func findOrder(id string) (*Order, error) {
if id == "" {
return nil, errors.New("id não pode ser vazio")
}
// ... busca no banco
order, found := cache[id]
if !found {
return nil, ErrNotFound // nil indica ausência
}
return order, nil
}
// Verificação do resultado antes de usar
order, err := findOrder("ord-42")
if err != nil {
log.Printf("erro: %v", err)
return
}
// Aqui order é garantidamente não-nil
fmt.Println(order.ID)
// Método em receiver nil — Go suporta, use com cuidado
type Node struct {
Value int
Next *Node
}
// Método pode ser chamado em nil receiver para encerrar recursão
func (n *Node) String() string {
if n == nil {
return "nil"
}
return fmt.Sprintf("%d -> %s", n.Value, n.Next.String())
}
// Lista ligada: 1 -> 2 -> nil
n := &Node{Value: 1, Next: &Node{Value: 2}}
fmt.Println(n.String()) // "1 -> 2 -> nil"
// Armadilha: campo ponteiro não inicializado
type Config struct {
Logger *slog.Logger
Timeout *time.Duration // evite ponteiros para tipos primitivos quando possível
}
var cfg Config
// cfg.Logger.Info("msg") // PANIC — Logger é nil
// Verifique antes ou inicialize no construtorValue receiver vs pointer receiver — tabela de decisão
O receiver de um método é apenas um parâmetro especial — a mesma semântica de cópia se aplica. A decisão entre value e pointer receiver afeta se o método pode modificar o struct, e determina onde a interface satisfaction funciona.
type BankAccount struct {
ID string
Balance float64
Owner string
}
// VALUE RECEIVER — trabalha com cópia
// ✓ Use quando: o método apenas lê (consulta) sem modificar
// ✓ Use quando: o tipo é pequeno e cópia é barata (ex: Point, Money)
// ✗ Evite quando: o struct é grande (cópia cara)
// ✗ Nunca funciona para modificar o struct original
func (a BankAccount) IsOverdrawn() bool {
return a.Balance < 0
}
func (a BankAccount) String() string {
return fmt.Sprintf("Account(%s: R$ %.2f)", a.ID, a.Balance)
}
// POINTER RECEIVER — trabalha com o original via endereço
// ✓ Use quando: o método modifica o struct
// ✓ Use quando: o struct é grande (evita cópia)
// ✓ Use quando: o método pode ser chamado em nil receiver
// ✓ Regra geral: se qualquer método precisa de pointer, use pointer em TODOS
func (a *BankAccount) Deposit(amount float64) error {
if amount <= 0 {
return errors.New("valor deve ser positivo")
}
a.Balance += amount // modifica o original
return nil
}
func (a *BankAccount) Withdraw(amount float64) error {
if amount > a.Balance {
return fmt.Errorf("saldo insuficiente: %.2f < %.2f", a.Balance, amount)
}
a.Balance -= amount
return nil
}
// Go converte automaticamente entre *T e T na chamada de métodos
acc := BankAccount{ID: "acc-1", Balance: 1000.0}
acc.Deposit(500.0) // Go converte para (&acc).Deposit(500.0)
fmt.Println(acc.IsOverdrawn()) // false
ptr := &BankAccount{ID: "acc-2", Balance: 500.0}
fmt.Println(ptr.String()) // Go converte para (*ptr).String()
// Tabela resumida de decisão
//
// Situação | Receiver
// -------------------------------------|----------
// Método modifica o struct | *T (obrigatório)
// Struct grande (>64 bytes) | *T (performance)
// Receiver pode ser nil | *T
// Satisfaz interface com mix de ambos | *T (seguro — *T satisfaz mais interfaces)
// Struct imutável pequena (Point, Money)| T ou *T — prefira consistência
// Tipo primitivo personalizado (type Km float64) | T geralmente OK
// Qualquer método do tipo já usa *T | *T (consistência — regra oficial)new() vs &T{} — qual usar e quando
new(T) e &T{} são funcionalmente equivalentes para structs: ambos alocam memória e retornam um *T. A diferença é que &T{} permite inicializar campos no literal. Na prática, &T{} é a forma idiomática e dominante em código Go real.
// new(T) — aloca memória zerada, retorna *T
// Todos os campos recebem zero value: 0, "", false, nil
p1 := new(Order)
fmt.Println(p1.Status) // "" (zero value de string)
fmt.Println(p1.Total) // 0.0
// &T{} — aloca e inicializa em um passo
p2 := &Order{
ID: "ord-1",
Status: "pending",
Total: 99.90,
}
// &T{} sem campos — equivalente a new(T)
p3 := &Order{} // todos os campos zerados, igual a new(Order)
// Para tipos primitivos — new() é mais útil
// (raro na prática, mas existe)
count := new(int)
*count = 42
fmt.Println(*count) // 42
// Caso de uso legítimo para new: sync.Mutex embutido
type SafeMap struct {
mu sync.Mutex
items map[string]string
}
sm := new(SafeMap) // zero value correto para Mutex
sm.items = make(map[string]string)
// vs &SafeMap{} — idêntico mas mais explícito sobre a inicialização
sm2 := &SafeMap{
items: make(map[string]string),
}
// Resumo:
// new(T) → use quando não precisa inicializar campos
// &T{} → use sempre que precisar inicializar campos (mais comum)
// &T{f: v} → expressivo, claro, idiomáticoEscape analysis — stack vs heap, -gcflags=“-m” para verificar
O compilador Go decide automaticamente se uma variável vive na stack (rápida, escopo limitado à função) ou no heap (GC gerenciado, mais lenta). Quando uma variável “escapa” — seu endereço é capturado além da função que a criou — o compilador a move para o heap. Você não controla isso diretamente, mas pode inspecionar as decisões do compilador.
// Variável fica na stack — escopo local, não escapa
func computeLocal() float64 {
x := 42.0 // x vive na stack — destruído quando a função retorna
return x // retorna o VALOR, não o endereço
}
// Variável escapa para o heap — endereço retornado
func newOrder(id string) *Order {
o := Order{ID: id, Status: "pending"} // o escapa para o heap
return &o // &o captura o endereço — compilador move o para heap
}
// Variável em closure escapa para o heap
func makeCounter() func() int {
count := 0 // count escapa — closure referencia seu endereço
return func() int {
count++
return count
}
}
// Variável em interface pode escapar
func printValue(v any) { // v any pode forçar escape de valores passados
fmt.Println(v)
}
// Canal — valores enviados podem escapar dependendo do uso
ch := make(chan *Order, 1)
go func() {
o := &Order{ID: "1"} // o provavelmente escapa para o heap
ch <- o
}()Para verificar o que escapa para o heap, use o flag -gcflags="-m":
# Analisa escapes no package atual
go build -gcflags="-m" ./...
# Saída exemplo:
# ./main.go:12:6: moved to heap: o
# ./main.go:18:2: moved to heap: count
# ./main.go:22:14: v escapes to heap
# Mais verboso — mostra razões detalhadas
go build -gcflags="-m -m" ./...// Por que isso importa para performance?
// - Stack allocation: praticamente zero custo, sem GC pressure
// - Heap allocation: custo de alocação + GC coleta quando não há mais referências
// Benchmark — stack vs heap
func BenchmarkStack(b *testing.B) {
for i := 0; i < b.N; i++ {
x := 42 // stack
_ = x
}
}
func BenchmarkHeap(b *testing.B) {
for i := 0; i < b.N; i++ {
x := new(int) // heap (new sempre aloca no heap? não necessariamente — o compilador pode otimizar)
*x = 42
_ = x
}
}
// Regra prática:
// - Não otimize prematuramente para stack vs heap
// - Use -gcflags="-m" para verificar quando performance é crítica
// - Evite retornar ponteiros para structs pequenas em hot paths
// - Prefira passar ponteiros a retorná-los quando possível em código de alta performanceSlices e maps são reference types — comportamento e armadilhas
Slices e maps contêm internamente um ponteiro para os dados. Quando você passa um slice ou map para uma função, está passando uma cópia do cabeçalho, mas o cabeçalho aponta para o mesmo array/bucket subjacente. Modificar elementos dentro de uma função afeta o original. Mas append pode criar um novo array — e aí surgem as armadilhas.
// ── SLICE ──────────────────────────────────────────────────────
// Cabeçalho do slice: [ptr | len | cap]
// Quando você copia um slice, copia o cabeçalho, não os dados
// Modificar elementos via função — funciona, afeta o original
func zeroFirst(s []int) {
s[0] = 0 // modifica o array subjacente compartilhado
}
nums := []int{1, 2, 3}
zeroFirst(nums)
fmt.Println(nums) // [0 2 3] — modificado
// append NÃO modifica o slice original se realocar
func appendItem(s []int, v int) []int {
return append(s, v) // pode criar novo array se cap insuficiente
}
nums2 := []int{1, 2, 3}
nums3 := appendItem(nums2, 4)
fmt.Println(nums2) // [1 2 3] — inalterado (new array foi criado)
fmt.Println(nums3) // [1 2 3 4] — novo slice
// Armadilha: slices compartilham array até o append realocar
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3] — b compartilha o array de a
b[0] = 99 // modifica a[1] também!
fmt.Println(a) // [1 99 3 4 5]
fmt.Println(b) // [99 3]
// Solução: use copy para criar slice independente
c := make([]int, 2)
copy(c, a[1:3])
c[0] = 0 // não afeta a
fmt.Println(a) // [1 99 3 4 5] — inalterado
// Para que uma função possa fazer append e o chamador veja:
// Opção 1: retornar o novo slice
func addItemReturn(s []int, v int) []int {
return append(s, v)
}
nums = addItemReturn(nums, 99)
// Opção 2: ponteiro para slice
func addItemPtr(s *[]int, v int) {
*s = append(*s, v)
}
addItemPtr(&nums, 99)
// ── MAP ────────────────────────────────────────────────────────
// Map já é um ponteiro internamente — modificações em função afetam o original
func addEntry(m map[string]int, key string, val int) {
m[key] = val // modifica o map original
}
scores := map[string]int{"Alice": 95}
addEntry(scores, "Bob", 87)
fmt.Println(scores) // map[Alice:95 Bob:87]
// Passar map por ponteiro é quase nunca necessário
// Exceto se você precisar substituir o mapa inteiro
func replaceMap(m *map[string]int) {
*m = make(map[string]int) // substitui o mapa
}Ponteiros para interfaces — antipadrão clássico
Uma das armadilhas mais comuns em Go: passar ponteiro para interface quando a interface já carrega o tipo concreto. Uma interface em Go é internamente um par (tipo, valor). Ponteiro para interface é **tipo — quase nunca é o que você quer.
// Interface e sua implementação
type Notifier interface {
Notify(msg string) error
}
type EmailNotifier struct {
From string
}
func (e *EmailNotifier) Notify(msg string) error {
fmt.Printf("Email de %s: %s\n", e.From, msg)
return nil
}
// CORRETO — passe a interface diretamente
func sendAlert(n Notifier, msg string) error {
return n.Notify(msg)
}
notifier := &EmailNotifier{From: "admin@example.com"}
sendAlert(notifier, "servidor em alerta") // OK
// ERRADO — ponteiro para interface é quase sempre um bug
func sendAlertWrong(n *Notifier, msg string) error { // *Notifier — raramente correto
return (*n).Notify(msg)
}
// Por que é errado?
// - *Notifier não satisfaz Notifier automaticamente
// - Impede que o chamador passe diretamente qualquer Notifier
// - Torna o código mais verbose sem benefício
// Quando *Notifier pode fazer sentido:
// Basicamente nunca em código de produção.
// Talvez ao precisar substituir a interface em si (raro):
func swapNotifier(n *Notifier, new Notifier) {
*n = new
}
// Armadilha: interface nil vs interface com valor nil
var email *EmailNotifier // ponteiro nil do tipo concreto
var n Notifier = email // interface NÃO é nil! Tem tipo (*EmailNotifier) mas valor nil
fmt.Println(email == nil) // true
fmt.Println(n == nil) // false — armadilha!
// n.Notify("msg") causaria panic (valor subjacente é nil)
// A solução é nunca atribuir ponteiro nil diretamente a uma interface
// Padrão correto: retorne nil de interface, não nil do tipo concreto
func createNotifier(useEmail bool) Notifier {
if !useEmail {
return nil // nil de interface — correto, comparável a nil
}
return &EmailNotifier{From: "app@example.com"}
}Passagem de funções como callbacks vs ponteiros
Funções são valores de primeira classe em Go. Callbacks por valor (passando func(...)) e callbacks por ponteiro (*func(...)) têm semânticas diferentes. Na grande maioria dos casos, passe a função por valor — é mais simples e suficiente. Ponteiro para função é raro e geralmente indica um design que pode ser melhorado.
// ── CALLBACKS POR VALOR — o padrão ────────────────────────────
// Tipo de função — nomeia para legibilidade
type ProcessFn func(ctx context.Context, order Order) error
type FilterFn func(order Order) bool
// Recebe callback por valor — simples, idiomático
func processOrders(ctx context.Context, orders []Order, process ProcessFn) error {
for _, o := range orders {
if err := process(ctx, o); err != nil {
return fmt.Errorf("order %s: %w", o.ID, err)
}
}
return nil
}
// Uso — passa função diretamente
err := processOrders(ctx, orders, func(ctx context.Context, o Order) error {
return notifyCustomer(ctx, o.CustomerID, "seu pedido foi processado")
})
// Pipeline funcional — funções como valores
pipeline := []FilterFn{
func(o Order) bool { return o.Total > 50 }, // filtro de valor mínimo
func(o Order) bool { return o.Status == "paid" }, // filtro de status
}
func applyFilters(orders []Order, filters []FilterFn) []Order {
result := make([]Order, 0, len(orders))
for _, o := range orders {
pass := true
for _, f := range filters {
if !f(o) {
pass = false
break
}
}
if pass {
result = append(result, o)
}
}
return result
}
// ── CLOSURES CAPTURAM POR REFERÊNCIA ──────────────────────────
// Closure captura a variável, não o valor — cuidado em loops (Go < 1.22)
func closureArmadilha() {
fns := make([]func(), 5)
for i := 0; i < 5; i++ {
i := i // cria nova variável por iteração (necessário em Go < 1.22)
fns[i] = func() { fmt.Println(i) }
}
// Go 1.22+: a redeclaração é desnecessária — cada iteração tem sua própria variável
for _, fn := range fns {
fn() // imprime 0, 1, 2, 3, 4
}
}
// ── PONTEIRO PARA FUNÇÃO — raro ───────────────────────────────
// Use quando precisar substituir a função em runtime via ponteiro
// (interface geralmente é uma abstração melhor)
type Handler struct {
Process *func(Order) error // raramente necessário
}
// Preferível: use interface
type Processor interface {
Process(Order) error
}Tabela resumida: tipo → usar ponteiro?
Esta tabela consolida todas as decisões de ponteiro em Go em um formato de consulta rápida.
// Exemplos de código por categoria:
// ① Struct mutável ou grande — use *T
type Service struct {
db *sql.DB
cache map[string]Order
}
func NewService(db *sql.DB) *Service { // retorna *Service — correto
return &Service{db: db, cache: make(map[string]Order)}
}
// ② Struct pequena imutável — T ou *T (consistência decide)
type Point struct{ X, Y float64 }
func Distance(a, b Point) float64 { // por valor — cópia de 16 bytes é barata
return math.Sqrt(math.Pow(b.X-a.X, 2) + math.Pow(b.Y-a.Y, 2))
}
// ③ Slice — passe por valor, retorne novo se append necessário
func filterActive(orders []Order) []Order { // slice por valor é normal
result := make([]Order, 0)
for _, o := range orders {
if o.Status == "active" {
result = append(result, o)
}
}
return result
}
// ④ Map — passe por valor (já é referência interna)
func updatePrices(catalog map[string]float64, increase float64) {
for k := range catalog {
catalog[k] *= (1 + increase) // modifica o mapa original — sem ponteiro necessário
}
}
// ⑤ Primitivo que precisa ser opcional — *T com nil como ausência
type Config struct {
MaxRetries *int // nil = "não configurado, use default"
Timeout *time.Duration
}
maxR := 3
cfg := Config{MaxRetries: &maxR}| Tipo | Usar ponteiro? | Razão |
|---|---|---|
| Struct grande (>64 bytes) | Sim, *T | Evita cópia cara em parâmetros e retornos |
| Struct mutável (qualquer tamanho) | Sim, *T | Método ou função precisa modificar os campos |
Struct pequena imutável (Point, Money) | Opcional | Por valor é mais simples; escolha por consistência |
Slice []T | Não | Já contém ponteiro interno; passe por valor, retorne novo slice se append |
Map map[K]V | Não | Já é referência internamente; modificações afetam o original |
Channel chan T | Não | Já é referência; ponteiro quase nunca faz sentido |
| Interface | Não | Interface já embala (tipo, valor); *Interface é antipadrão |
string | Não | Imutável; cópia do header (16 bytes) é barata |
int, float64, bool | Raramente | Apenas quando nil tem semântica (“campo ausente” em config/JSON) |
Função func(...) | Não | Função é valor imutável; passe por valor |
| Retorno de construtor | Sim, *T | func New() *T é o padrão idiomático para structs |
Versões — O que mudou em relação a ponteiros e memória
| Versão | Ano | Mudanças relevantes para ponteiros e memória |
|---|---|---|
| 1.17 | 2021 | Registro de argumentos em ABI — Go passou a usar registradores em vez de stack para passar argumentos em x86-64/ARM64, reduzindo overhead de chamadas de função |
| 1.18 | 2022 | Generics — type parameters mudaram como ponteiros interagem com tipos parametrizados; any como alias de interface{} |
| 1.20 | 2023 | Conversão entre slice e array com ponteiro: (*[N]T)(slice) ou [N]T(slice) (conversão direta sem unsafe); melhorias no GC com arenas experimentais |
| 1.21 | 2023 | Melhorias no GC — menor latência de pausa; otimizações de escape analysis para closures comuns |
| 1.22 | 2024 | Cada iteração de loop cria sua própria variável — elimina a armadilha clássica de closure capturando variável de loop por referência. Código pré-1.22 que redeclarava i := i pode ser simplificado |
| 1.23 | 2024 | iter package — iteradores com func(yield func(K,V) bool) mudaram como ponteiros são capturados em iteradores customizados; unique package para interning de valores comparáveis |