Backend

Go — Ponteiros

Referência completa sobre ponteiros em Go — pass-by-value, operadores, escape analysis, receivers, armadilhas e quando usar

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)    // 10

Pointer 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 construtor

Value 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ático

Escape 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 performance

Slices 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}
TipoUsar ponteiro?Razão
Struct grande (>64 bytes)Sim, *TEvita cópia cara em parâmetros e retornos
Struct mutável (qualquer tamanho)Sim, *TMétodo ou função precisa modificar os campos
Struct pequena imutável (Point, Money)OpcionalPor valor é mais simples; escolha por consistência
Slice []TNãoJá contém ponteiro interno; passe por valor, retorne novo slice se append
Map map[K]VNãoJá é referência internamente; modificações afetam o original
Channel chan TNãoJá é referência; ponteiro quase nunca faz sentido
InterfaceNãoInterface já embala (tipo, valor); *Interface é antipadrão
stringNãoImutável; cópia do header (16 bytes) é barata
int, float64, boolRaramenteApenas quando nil tem semântica (“campo ausente” em config/JSON)
Função func(...)NãoFunção é valor imutável; passe por valor
Retorno de construtorSim, *Tfunc New() *T é o padrão idiomático para structs

Versões — O que mudou em relação a ponteiros e memória

VersãoAnoMudanças relevantes para ponteiros e memória
1.172021Registro 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.182022Generics — type parameters mudaram como ponteiros interagem com tipos parametrizados; any como alias de interface{}
1.202023Conversã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.212023Melhorias no GC — menor latência de pausa; otimizações de escape analysis para closures comuns
1.222024Cada 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.232024iter 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