Passagem por valor vs por ponteiro

1. Fundamentos da Passagem por Valor em Go

Em Go, tudo é passado por valor. Quando você passa um argumento para uma função, uma cópia do valor original é criada. Isso é verdade para tipos primitivos como int, string e bool:

func modificarValor(x int) {
    x = 100
    fmt.Println("Dentro da função:", x) // 100
}

func main() {
    numero := 10
    modificarValor(numero)
    fmt.Println("Fora da função:", numero) // 10 (original permanece)
}

Para structs pequenas, o comportamento é idêntico — todos os campos são copiados:

type Ponto struct {
    X, Y int
}

func mover(p Ponto) {
    p.X += 10
    p.Y += 10
}

func main() {
    p := Ponto{1, 2}
    mover(p)
    fmt.Println(p) // {1 2} (original não foi alterado)
}

A implicação de desempenho é clara: cópias são seguras e previsíveis, mas podem ser caras para estruturas grandes. Para tipos pequenos (até alguns bytes), a cópia é geralmente mais rápida que seguir um ponteiro.

2. Passagem por Ponteiro: Endereços e Mutabilidade

Para modificar o valor original, usamos ponteiros com a sintaxe *T (tipo ponteiro) e & (endereço):

func incrementar(ptr *int) {
    *ptr++ // desreferencia e modifica o valor original
}

func main() {
    valor := 42
    incrementar(&valor)
    fmt.Println(valor) // 43 (modificado!)
}

Ponteiros são essenciais quando:
- Precisamos modificar o valor original dentro da função
- A estrutura de dados é grande (ex.: struct com 100+ campos)

type Usuario struct {
    Nome    string
    Email   string
    Senha   string
    // ... mais 50 campos
}

func atualizarEmail(u *Usuario, novoEmail string) {
    u.Email = novoEmail // modifica diretamente o original
}

3. A Armadilha das Referências: Slices, Maps e Canais

Slices, maps e canais parecem referências, mas não são. O que acontece é que o cabeçalho do slice (ponteiro para array subjacente, comprimento e capacidade) é copiado, mas o array subjacente é compartilhado:

func modificarSlice(s []int) {
    s[0] = 999 // modifica o array subjacente compartilhado
    s = append(s, 4) // cria um novo array se necessário (não afeta o original)
}

func main() {
    original := []int{1, 2, 3}
    modificarSlice(original)
    fmt.Println(original) // [999 2 3] (primeiro elemento foi alterado!)
}

Maps e canais já são tipos de referência internamente. Passá-los por valor ou ponteiro geralmente não faz diferença prática:

func adicionarChave(m map[string]int) {
    m["novo"] = 42 // modifica o map original
}

4. Ponteiros com Structs Aninhadas e Campos Anônimos

Ponteiros facilitam a navegação em estruturas aninhadas complexas, como árvores binárias:

type No struct {
    Valor int
    Esq   *No
    Dir   *No
}

func inserir(raiz *No, valor int) *No {
    if raiz == nil {
        return &No{Valor: valor}
    }
    if valor < raiz.Valor {
        raiz.Esq = inserir(raiz.Esq, valor)
    } else {
        raiz.Dir = inserir(raiz.Dir, valor)
    }
    return raiz
}

Com campos anônimos, a promoção de métodos funciona naturalmente:

type Logger struct{}
func (l *Logger) Log(msg string) { fmt.Println(msg) }

type Servico struct {
    *Logger // campo anônimo ponteiro
    Nome string
}

func main() {
    s := &Servico{Logger: &Logger{}, Nome: "API"}
    s.Log("Iniciando serviço") // método promovido do Logger
}

5. Funções com Múltiplos Retornos e Ponteiros

Named returns podem interagir de forma inesperada com ponteiros:

func criarUsuario(nome string) (u *Usuario) {
    // u é um ponteiro para Usuario, inicializado como nil
    return // retorna nil se não atribuirmos nada
}

func processar(nome string) (*Resultado, error) {
    if nome == "" {
        return nil, ErrNomeVazio // padrão comum para indicar ausência
    }
    return &Resultado{Nome: nome}, nil
}

Retornar ponteiro para variável local é seguro em Go (escape analysis move para heap):

func novoPonto(x, y int) *Ponto {
    p := Ponto{x, y} // alocado na stack? Não! Escapa para heap
    return &p        // Go move p para heap automaticamente
}

6. Boas Práticas e Decisões de Design

A regra de ouro:
- Por valor: tipos pequenos (int, bool, structs com poucos campos), dados imutáveis
- Por ponteiro: structs grandes, dados que precisam ser modificados, campos opcionais (nil)

Consistência de API é crucial. Se um método tem receiver por ponteiro, todos os métodos do mesmo tipo devem seguir o padrão:

type Conta struct {
    Saldo float64
}

// Bom: todos os métodos usam *Conta
func (c *Conta) Depositar(valor float64) { c.Saldo += valor }
func (c *Conta) Sacar(valor float64) bool {
    if c.Saldo >= valor {
        c.Saldo -= valor
        return true
    }
    return false
}

Evite ponteiros desnecessários — eles dificultam a leitura e podem confundir o garbage collector.

7. Casos Especiais: Funções Variádicas e Interfaces

Funções variádicas recebem uma cópia do slice:

func somar(numeros ...int) int {
    total := 0
    for _, n := range numeros {
        total += n
    }
    return total
}

Interfaces são um caso especial. Um tipo *T implementa uma interface, mas T pode não implementar:

type Stringer interface {
    String() string
}

type MeuTipo struct {
    Nome string
}

func (m *MeuTipo) String() string {
    return m.Nome
}

func main() {
    var s Stringer
    // s = MeuTipo{"teste"} // ERRO: MeuTipo não implementa Stringer
    s = &MeuTipo{"teste"} // OK: *MeuTipo implementa
}

8. Depuração e Armadilhas Comuns

Em concorrência, ponteiros compartilhados causam data races:

func main() {
    var contador int
    for i := 0; i < 1000; i++ {
        go func() {
            contador++ // data race! contador é compartilhado
        }()
    }
}

O escape analysis do Go decide onde alocar:

func f() *int {
    x := 42
    return &x // x escapa para heap (análise do compilador)
}

func g() {
    y := 42
    fmt.Println(y) // y fica na stack (não escapa)
}

Benchmarks mostram a diferença de desempenho:

func BenchmarkPorValor(b *testing.B) {
    p := PontoGrande{/* muitos campos */}
    for i := 0; i < b.N; i++ {
        processarValor(p)
    }
}

func BenchmarkPorPonteiro(b *testing.B) {
    p := &PontoGrande{/* muitos campos */}
    for i := 0; i < b.N; i++ {
        processarPonteiro(p)
    }
}

Referências