Toda vez que você chama uma função, a linguagem precisa decidir o que entregar a ela. A função recebe o próprio valor ou uma forma de acessar o original? Essa decisão — call by value versus call by reference — determina se alterações feitas dentro da função são visíveis para quem a chamou. Go tem uma resposta precisa: é estritamente call by value, sempre. Mas essa resposta esconde uma sutileza que pega muitos desenvolvedores de surpresa, especialmente ao trabalhar com maps e slices.
Call by value
Quando uma linguagem usa call by value, ela passa uma cópia do argumento para a função. A função trabalha com sua própria cópia privada, e qualquer coisa que ela faça nessa cópia não tem efeito sobre a variável original no chamador.
double recebe uma cópia de x. Multiplicar essa cópia por dois não altera nada em main. Quando a função retorna, a cópia é descartada e x permanece intocado.
Call by value é o padrão para tipos primitivos na maioria das linguagens — integers, floats, booleans e similares. É simples de raciocinar: o chamador mantém controle total sobre suas variáveis, e nenhuma função pode surpreendê-lo modificando algo que deveria apenas ler.
Call by reference
Quando uma linguagem usa call by reference, ela passa o endereço de memória do argumento. A função não recebe uma cópia — ela recebe acesso direto à mesma memória que o chamador está usando. Qualquer modificação que a função faça é imediatamente visível no chamador.
Linguagens como C++ suportam call by reference explicitamente. Java e Python usam uma forma disso para objetos: a referência (o endereço do objeto) é copiada, não o objeto em si — uma sutileza à qual voltaremos ao falar sobre Go.
A diferença na prática:
| Call by value | Call by reference | |
|---|---|---|
| O que é passado | Uma cópia do valor | O endereço de memória |
| Alterações dentro da função | Afetam apenas a cópia local | Afetam a variável original |
| Uso típico | Tipos primitivos | Estruturas grandes ou compartilhadas |
Go: estritamente call by value
Go não possui call by reference. Todo argumento que você passa para uma função é copiado. Isso não é uma simplificação — é a regra precisa. Mesmo ao passar um pointer, o próprio pointer é copiado. A função recebe sua própria cópia do endereço, não uma referência à variável do pointer.
A função altera sua cópia local de p para nil, mas o ptr original em main não é afetado. Duas cópias do pointer existiram durante a chamada, e modificar uma não toca a outra.
No entanto, se ambas as cópias do pointer contêm o mesmo endereço, ambas podem ser usadas para modificar o valor naquele endereço:
Essa é a distinção fundamental: Go é estritamente call by value, mas valores podem ser endereços. Passar um pointer passa uma cópia do endereço — e uma cópia de um endereço ainda aponta para o mesmo lugar.
Maps
Maps em Go se comportam de uma forma que surpreende muitos iniciantes: alterações feitas em um map dentro de uma função são visíveis para o chamador, mesmo que Go seja call by value. Para entender o porquê, é preciso saber o que um map realmente é.
Um map não é um valor simples. É uma estrutura de dados que contém, entre outras coisas, um pointer para a hash table subjacente onde os pares chave-valor estão armazenados. Quando você passa um map para uma função, Go copia o header do map — uma pequena struct — e essa cópia inclui o mesmo pointer para a mesma hash table.
Tanto main quanto addKey possuem cópias do header do map, e ambos os headers apontam para o mesmo armazenamento subjacente. Inserir uma chave dentro da função escreve nesse armazenamento compartilhado, então o chamador vê a alteração.
Maps não são reference types
É comum ouvir maps descritos como "reference types" em Go. É um modelo mental útil, mas tecnicamente Go não possui reference types. O que Go tem são valores que internamente contêm pointers. O efeito é semelhante, mas o mecanismo é cópia-de-header, não pass-by-reference.
Reatribuir uma variável de map não se propaga para o chamador
Se você atribuir um map completamente novo ao parâmetro dentro da função, o chamador não verá a mudança. Você está apenas alterando para qual map a cópia local do header aponta.
Slices
Slices seguem o mesmo padrão dos maps, com uma complicação importante. Um slice header contém três campos: length, capacity e um pointer para o backing array. Quando você passa um slice para uma função, uma cópia desse header é feita. A cópia compartilha o mesmo backing array.
Modificar um elemento existente através da cópia local escreve no backing array compartilhado e é visível para o chamador:
Mas append é diferente. Quando append precisa de mais capacity do que o backing array atual fornece, ele aloca um novo array, copia os elementos existentes e retorna um novo slice header apontando para o novo array. O backing array original no chamador não é afetado.
A cópia local do header na função é atualizada para apontar para o novo array, mas o header em main ainda aponta para o original. O 99 se perde quando a função retorna.
Se você precisa que uma função expanda um slice e o chamador veja o resultado, retorne o novo slice e atribua no ponto de chamada:
append dentro da capacity existente
Se o slice tem capacity suficiente para o novo elemento sem precisar crescer, append escreve no backing array existente e não aloca novo espaço. Nesse caso, tanto o chamador quanto a função enxergam os mesmos dados subjacentes. Esse caso especial é uma fonte de bugs sutis — é mais seguro sempre tratar o valor de retorno de append como o slice autoritativo.
Juntando tudo
A regra de call by value do Go é uniforme e consistente. O que cria a aparência de semântica de referência para maps e slices é que seus headers contêm pointers para armazenamento compartilhado. Entender esse modelo explica todo comportamento observável:
| Tipo | O que é copiado | Mutações visíveis ao chamador? | Reatribuição visível? |
|---|---|---|---|
int, bool, string | O próprio valor | Não | Não |
Pointer (*T) | O endereço | Sim (via *p) | Não |
| Map | Header + pointer para tabela compartilhada | Sim | Não |
| Slice | Header (len, cap, pointer) | Sim (escrita por índice) | Não |
Slice + append (cresce) | Header — novo array alocado | Não | Não |
A conclusão prática: trate Go como estritamente call by value, entenda que alguns valores contêm endereços, e você nunca será surpreendido pelo que uma função pode ou não modificar.