Por que Go copia tudo? Zero Values e Pointer Semantics
Entenda zero values, value semantics e pointer semantics em Go: o que são, quando copiar por valor ou passar por ponteiro e como escolher, com exemplos práticos.
Se você está começando com Go vindo de outra linguagem, provavelmente já esbarrou em duas coisas que parecem simples mas mudam a forma como você escreve código: os zero values e a decisão entre passar dados por valor ou por ponteiro.
À primeira vista são dois assuntos separados. Mas eles fazem parte da mesma filosofia da linguagem, e entender um ajuda a entender o outro. A proposta deste artigo é construir esse entendimento de forma gradual: primeiro o que é um zero value e por que ele existe, depois o que significam value e pointer semantics.
A filosofia do Go: variável declarada é variável pronta!
Em muitas linguagens, declarar uma variável e não atribuir um valor a ela é um problema. Veja em C:
int x;
printf("%d\n", x); // valor imprevisível ("lixo de memória")
Aqui x foi declarada, mas não inicializada. O que existe naquele endereço de memória é o que sobrou de antes, um valor imprevisível. Ler uma variável automática não inicializada é justamente o que a especificação da linguagem chama de comportamento indefinido.
Em Java o problema aparece de outra forma: campos de referência são inicializados automaticamente como null.
class Sessao {
User user; // campo de referência começa como null
void processar() {
user.getName(); // NullPointerException em tempo de execução
}
}
O resultado é que você acaba escrevendo verificações defensivas em todo lugar (if (user != null)) para não quebrar em produção.
Go decidiu resolver isso na raiz com uma regra simples:
Não existe "lixo de memória" e não existe variável não inicializada. Isso não é um detalhe de implementação, é uma decisão de design pensada para que o código seja previsível por padrão.
Parte 1: O que são Zero Values
Quando você declara uma variável sem valor, Go a inicializa com o zero value do tipo dela:
var age int // 0
var price float64 // 0.0
var name string // "" (string vazia)
var active bool // false
var list []int // nil
var user *User // nil
O zero value é determinístico: para um dado tipo, ele é sempre o mesmo. Isso significa que você pode raciocinar sobre o estado inicial de qualquer variável só olhando o tipo dela.
Aqui está a tabela que vale a pena guardar:
| Categoria de tipo | Zero value |
|---|---|
Numéricos (int, float64, ...) | 0 |
bool | false |
string | "" |
| Ponteiros, slices, maps, channels, funcs, interfaces | nil |
struct | cada campo com seu próprio zero value |
O caso do struct é o mais interessante, porque a regra se aplica recursivamente:
type User struct {
Nome string
Idade int
Ativo bool
}
var u User
fmt.Printf("%+v\n", u) // {Nome: Idade:0 Ativo:false}
Não precisei escrever um construtor nem inicializar campo por campo. A struct já nasce em um estado válido e conhecido.
O padrão do "zero value útil"
Em Go, um bom design faz o zero value ser imediatamente utilizável, sem exigir inicialização.
O exemplo clássico é sync.Mutex:
var mu sync.Mutex
mu.Lock() // funciona. Não precisou de sync.NewMutex()
mu.Unlock()
O mesmo vale para bytes.Buffer:
var buf bytes.Buffer
buf.WriteString("olá") // já funciona, buffer vazio é um estado válido
Quando você projetar suas próprias structs, vale perguntar: "o zero value dessa struct faz sentido sozinho?". Se a resposta for sim, você poupa quem usa seu código de uma etapa de inicialização.
map com zero value é nil, e escrever em um map nil causa panic. Nesses casos você ainda precisa inicializar com make(map[string]int). Ler de um map nil, porém, é seguro e retorna o zero value.Antes de seguir, uma armadilha comum que combina zero value com um dos temas do próximo tópico:
var list []int
list = append(list, 1) // OK! append lida com slice nil de boa
fmt.Println(list) // [1]
Um slice nil tem comprimento zero e você pode dar append nele normalmente. Isso é diferente do map, e é o tipo de detalhe que só fica natural com o tempo.
Parte 2: Value Semantics
Agora vamos à segunda metade. Em Go, quando você passa um valor para uma função ou atribui a outra variável, por padrão uma cópia é feita. Isso é o que chamamos de value semantics.
func dobrar(n int) {
n = n * 2 // altera a CÓPIA local
}
func main() {
x := 10
dobrar(x)
fmt.Println(x) // 10 — o original não mudou
}
A função dobrar recebeu uma cópia de x. Alterá-la lá dentro não tem efeito nenhum sobre o x do main. O mesmo acontece com structs:
func aniversario(u User) {
u.Idade++ // altera a cópia
}
func main() {
joao := User{Nome: "João", Idade: 30}
aniversario(joao)
fmt.Println(joao.Idade) // 30 — inalterado
}
A vantagem de value semantics é o isolamento: cada função trabalha com seu próprio dado e não há como uma parte do código modificar acidentalmente um valor que pertence a outra. O código fica mais fácil de raciocinar, porque você sabe que passar algo adiante não vai alterá-lo pelas suas costas.
Parte 3: Pointer Semantics
Mas e quando você quer que a alteração seja refletida no original? Ou quando copiar o dado inteiro seria caro? É aí que entram os ponteiros.
Um ponteiro guarda o endereço de um valor, não o valor em si. Usamos & para pegar o endereço de uma variável e * para acessar o valor apontado.
func aniversario(u *User) {
u.Idade++ // altera o ORIGINAL, através do endereço
}
func main() {
joao := User{Nome: "João", Idade: 30}
aniversario(&joao) // passamos o endereço
fmt.Println(joao.Idade) // 31 — mudou de verdade
}
Agora a função recebeu o endereço de joao. Ao alterar u.Idade, ela está mexendo no mesmo dado que existe no main. Isso é pointer semantics: você compartilha o acesso ao valor em vez de copiá-lo.
u.Idade não precisei escrever (*u).Idade. Go faz esse "de-referenciamento" automaticamente para campos de struct acessados por ponteiro. É um syntactic sugar que deixa o código mais limpo.Existem dois motivos principais para usar ponteiros:
- Mutação compartilhada: quando a função precisa modificar o valor original.
- Evitar cópias caras: quando a struct é grande, copiá-la a cada chamada custa memória e tempo.
Um detalhe que muda tudo: ponteiro também é valor
Aqui vai a sacada que costuma reorganizar o entendimento: em Go, tudo é passado por valor, sempre. Não existe "passagem por referência" como em algumas outras linguagens. Quando você passa um ponteiro, o que acontece é que o valor do ponteiro (ou seja, o endereço) é copiado.
u := User{Nome: "João"}
p := &u // p guarda o endereço de u
q := p // q é uma CÓPIA do endereço, não "referencia" p
q não aponta para p. q recebeu uma cópia do endereço que estava em p. Os dois apontam para o mesmo User, mas são dois ponteiros independentes. No fundo, pointer semantics é só value semantics aplicada a um valor que por acaso é um endereço.
Isso também explica por que alguns tipos parecem se comportar de forma diferente. Quando você copia uma struct ou um array, copia o conteúdo inteiro: são valores autocontidos. Já slice, map, channel e func carregam uma referência interna, então copiar um slice copia apenas o seu cabeçalho (ponteiro, tamanho e capacidade), não o array por baixo. É por isso que dois slices podem enxergar os mesmos dados mesmo sem você usar ponteiro explicitamente. Esse comportamento rende um artigo só pra ele, e é o que pretendo destrinchar em um próximo post.
Parte 4: Como escolher entre value e pointer semantics
Ao longo dos anos, a comunidade e os mantenedores do Go consolidaram algumas diretrizes bem práticas (Effective Go, o Code Review Comments do Go Wiki, e gente como Dave Cheney e William Kennedy).
A regra mais importante vem do próprio time do Go: seja consistente. A recomendação do Go Wiki é clara: se algum método de um tipo precisa de receiver por ponteiro, então normalmente todos deveriam usar ponteiro. Misturar value e pointer no mesmo tipo sem necessidade é fonte de bugs sutis.
Como ponto de partida:
- A função precisa modificar o valor recebido.
- A struct é grande e a cópia seria custosa.
- O tipo representa algo que não deve ser copiado (como um
sync.Mutex, uma conexão, um buffer com estado).
- O tipo é pequeno e representa um dado (um ponto, uma cor, um
time.Time). - Você quer garantir imutabilidade e isolamento.
- Não há necessidade de modificar o original.
Um exemplo de tipo que idiomaticamente usa value semantics é time.Time. Ele é passado por valor em toda a biblioteca padrão, mesmo sendo uma struct, porque representa um instante, um dado, não algo com estado mutável compartilhado.
Já tipos como *sql.DB, *http.Request ou os que carregam um sync.Mutex usam pointer semantics, porque copiá-los seria errado ou perigoso.
Um cuidado importante: ponteiros e nil
Quando você adota pointer semantics, o zero value volta a aparecer e pode te passar uma rasteira. O zero value de um ponteiro é nil, e acessar um ponteiro nil causa panic:
var u *User // nil
fmt.Println(u.Nome) // panic: invalid memory address
Repare no ciclo completo: foi justamente esse tipo de problema que os zero values resolveram para valores, mas ele reaparece assim que você introduz ponteiros. Por isso a escolha por pointer semantics não é gratuita, ela traz de volta a necessidade de pensar em nil. É mais um argumento a favor de preferir value semantics quando você não tem um motivo claro para o contrário.
Juntando tudo: o caminho mental
Se eu tivesse que resumir o raciocínio em uma sequência de perguntas na hora de escrever código Go, seria assim:
- Qual o estado inicial natural do meu tipo? Se o zero value já for utilizável, aproveite, menos inicialização, menos código.
- Essa função precisa alterar o valor original? Se sim, ponteiro. Se não, comece pensando em valor.
- O valor é grande ou não pode ser copiado? Se sim, ponteiro, mesmo sem mutação.
- Estou sendo consistente com o resto do tipo? Se um método precisa de ponteiro, normalmente todos os outros também deveriam usar ponteiro.
- Ao usar ponteiros, tratei o caso
nil? O zero value voltou; não deixe ele te pegar.
Zero values e semântica de passagem não são tópicos isolados: são duas faces da mesma ideia de que, em Go, o estado de um valor deve ser sempre previsível. Value semantics preserva o isolamento dos dados: cada parte do código trabalha com sua própria cópia. Pointer semantics abre mão desse isolamento de propósito, para permitir compartilhamento de estado entre diferentes partes do código, e é justamente por isso que exige mais cuidado.
Referências
- The Go Programming Language Specification — The zero value — definição oficial de zero values na spec da linguagem.
- Effective Go — Data / Allocation — como o Go trata inicialização,
new,makee o padrão do zero value útil. - Go Blog — The Laws of Reflection — contexto sobre valores, tipos e como Go os representa internamente.
- Ardan Labs — Value and Pointer Semantics (William Kennedy) — a referência mais citada sobre a filosofia de escolher entre value e pointer semantics.
- Go Wiki — Code Review Comments: Receiver Type — diretrizes oficiais sobre quando usar receivers por valor ou por ponteiro.
- Dave Cheney — Should methods be declared on T or *T — discussão aprofundada sobre consistência de semântica em métodos.
- A Tour of Go — Pointers — introdução interativa e oficial a ponteiros.