Home
Apr 12, 2026 - 10 MIN READ
Por que Go copia tudo? Zero Values e Pointer Semantics

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:

Toda variável declarada em Go sempre recebe um valor inicial bem definido, mesmo que você não atribua nada. Esse valor é o zero value do tipo.

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 tipoZero value
Numéricos (int, float64, ...)0
boolfalse
string""
Ponteiros, slices, maps, channels, funcs, interfacesnil
structcada 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.

Nem todo zero value é útil de imediato. Um 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.

Note que em 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:

  1. Mutação compartilhada: quando a função precisa modificar o valor original.
  2. 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:

Use pointer semantics quando:
  • 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).
Use value semantics quando:
  • 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:

  1. Qual o estado inicial natural do meu tipo? Se o zero value já for utilizável, aproveite, menos inicialização, menos código.
  2. Essa função precisa alterar o valor original? Se sim, ponteiro. Se não, comece pensando em valor.
  3. O valor é grande ou não pode ser copiado? Se sim, ponteiro, mesmo sem mutação.
  4. Estou sendo consistente com o resto do tipo? Se um método precisa de ponteiro, normalmente todos os outros também deveriam usar ponteiro.
  5. 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

Copyright © 2026 Jairo Blatt