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
- Effective Go - Pointers vs. Values — Documentação oficial explicando quando usar ponteiros vs valores em Go
- Go by Example: Pointers — Tutoriais práticos com exemplos de ponteiros em Go
- The Go Blog: Pointers vs. Values in Go — Artigo oficial do time Go sobre a semântica de ponteiros e valores
- Dave Cheney: Pointers in Go — Análise aprofundada sobre boas práticas com ponteiros em Go
- Golang Specification: Pass by Value — Especificação oficial da linguagem sobre chamadas de função e passagem de argumentos
- Golang Book: Pointers and Memory Allocation — Capítulo sobre ponteiros e alocação de memória em Go
- Uber Go Style Guide: Pointers — Guia de estilo da Uber sobre uso de ponteiros em Go