Fundamentos de Programação/01 - Fundamentos Duros/Semana 01 - Diagnóstico e o que Acontece por Baixo10 min
Passagem por Valor vs por Referência
- Sua linguagem passa argumentos por valor, por referência, ou por valor-da-referência?
- Depois de
f(x), em que casosxmudou para quem chamou? - Por que
a = bàs vezes copia e às vezes aliasa? (é o entregável da semana) - Como você provaria isso com um teste em 5 linhas?
Conceito
A pergunta é uma só: quando uma função recebe um argumento, ela recebe uma cópia do valor ou um alias para a variável de quem chamou?
Por valor. A função recebe uma cópia. Escrever no parâmetro é invisível para quem chamou.
Por referência. A função recebe um alias para o armazenamento de quem chamou.
Escrever no parâmetro muda a variável de quem chamou. É C++ int&, Pascal var,
C# ref, Fortran.
Passar um ponteiro por valor não é passagem por referência.
Nos dois casos você consegue mutar o objeto apontado. Só na passagem por referência você consegue religar a variável de quem chamou a outro objeto.
O teste que separa os dois: a função consegue fazer a variável de quem chamou apontar para um objeto diferente? Por referência: sim. Ponteiro por valor: não — você só religou a sua cópia do ponteiro.
Java, Python, JavaScript, Go e C# (sem ref) fazem ponteiro por valor, e todas as
cinco são rotineiramente descritas como "por referência". O nome correto para esse
comportamento é call by sharing: você e a função compartilham o objeto, mas cada
um tem sua própria variável apontando pra ele.
Cópia rasa é a regra universal. Copiar um valor copia exatamente a representação dele. Se a representação contém uma referência, a cópia compartilha o referente. Nenhuma linguagem mainstream copia profundamente por padrão — e por bons motivos: cópia profunda é caríssima, e frequentemente indefinida (o que copiar de um socket aberto? de um grafo com ciclo?).
Consequência prática: a semântica de mutação não está na sintaxe da chamada, está
no tipo do argumento. Olhar f(x) não te diz nada — é a assinatura e o tipo que
decidem.
Em Go
Go passa tudo por valor. Sempre. Sem exceção.
A palavra referência cobre duas coisas diferentes, e só uma delas falta em Go:
| O que é | Go tem? | |
|---|---|---|
| Referência como valor | um valor que denota outro lugar: o tipo *T, o que &x produz |
sim, e é central |
| Passagem por referência | modo de passagem em que o parâmetro é a variável do chamador | não |
* faz exatamente o que você espera: *T é o tipo ponteiro, *p desreferencia,
&x pega o endereço. O que não existe é o void f(int& x) do C++ — um parâmetro
que seja a variável de quem chamou, sem ponteiro no meio.
A prova de que ponteiro é passado por valor: você consegue mutar o apontado, mas não consegue religar o ponteiro do chamador — para isso precisa de mais um nível de indireção. Essa necessidade é a prova.
func mutaApontado(p *int) { *p = 99 } // muta o APONTADO: funciona
func tentaReligar(p *int) {
outro := 42
p = &outro // religa a CÓPIA local de p. Invisível para quem chamou.
}
func religaDeVerdade(pp **int) {
outro := 42
*pp = &outro // agora sim
}
func main() {
x := 1
p := &x
antes := p
mutaApontado(p)
fmt.Println(x) // 99 <- o apontado mudou
tentaReligar(p)
fmt.Println(p != antes) // false <- p NÃO mudou
religaDeVerdade(&p)
fmt.Println(p != antes, *p) // true 42
}
Em C++ o equivalente de **int é int*&, e ele não é necessário: lá o parâmetro
já é a variável do chamador. Em Go, * te dá uma referência como valor que você
passa — nunca uma referência como modo de passar.
"Mas se é por valor, como *p = 99 muda o x?"
Porque p também é uma variável: ela tem endereço próprio e guarda um valor, e
esse valor por acaso é um endereço. Passagem por valor copia o valor que ela
guarda — não a coisa que esse valor aponta.
Imprimir os endereços torna a cópia visível:
func recebe(p *int) {
fmt.Printf(" funcao: p VALE %p | p MORA EM %p\n", p, &p)
*p = 99
}
func main() {
x := 1
p := &x
fmt.Printf("main: p VALE %p | p MORA EM %p\n", p, &p)
recebe(p)
fmt.Println(x) // 99
}
main: p VALE 0x...826128 | p MORA EM 0x...816040
funcao: p VALE 0x...826128 | p MORA EM 0x...982000
^ mesmo valor ^ endereço DIFERENTE
Os endereços mudam a cada execução — o que importa é que a segunda coluna difere:
p MORA EM não é o mesmo nos dois, ou seja, são duas variáveis distintas. Essa é a
cópia, tornada visível.
O que foi copiado foi o número 0x...826128 — o endereço de x.
| Operação | Escreve onde | Atravessa a cópia? |
|---|---|---|
*p = 99 |
no endereço escrito em p (isto é, em x) |
sim — as duas cópias guardam o mesmo endereço |
p = &outro |
em p (na variável local da função) | não — cada cópia é uma variável separada |
A analogia: p é um papel com um endereço escrito. Passar p dá à função uma
fotocópia do papel. *p = 99 é "vá ao endereço escrito no papel e pinte a casa" —
a fotocópia tem o mesmo endereço, a casa é a mesma, você vê a pintura. p = &outro é
"apague o papel e escreva outro endereço" — só a fotocópia muda. Para a função alterar
o seu papel, você entrega o endereço do papel: &p, tipo **int.
A escada de alcance — passagem por valor nunca é violada; cada * compra um degrau:
| Assinatura | O que é copiado | Até onde alcança |
|---|---|---|
f(v int) |
o número | nada do chamador |
f(p *int) |
um endereço | 1 degrau: o int apontado |
f(pp **int) |
o endereço de um ponteiro | 2 degraus: o ponteiro e o int |
E é assim que se reconhece passagem por referência de verdade: ela dá um degrau de
alcance sem você escrever *, e sem & no ponto de chamada. Em C++,
void f(int& x) chamado como f(y) altera y, e nada na chamada avisa. Em Go você é
obrigado a escrever f(&y) — a transferência do direito de mutação fica visível na
chamada. Essa é a vantagem real de só existir passagem por valor.
A segunda fonte de confusão: alguns tipos contêm ponteiros na própria representação. Copiar o valor copia o ponteiro fielmente, e as duas cópias passam a olhar o mesmo dado:
| Tipo | O que a = b copia |
Mutação visível pra quem chamou? |
|---|---|---|
int, bool, float64, struct, array |
todos os bytes | não |
*T |
o endereço (8 bytes) | sim, via *p |
[]T |
header: ptr + len + cap (24 bytes) | elementos sim; append não |
map[K]V |
ponteiro pro hmap (8 bytes) | sim |
chan T |
ponteiro pro hchan (8 bytes) | sim |
string |
header: ptr + len (16 bytes) | não (imutável) |
func |
ponteiro pro closure | via variáveis capturadas |
interface |
(tipo, dado) — 16 bytes | depende do que está dentro |
Go é honesto onde Java não é: em Java, Integer e ArrayList têm sintaxe idêntica e
semântica de cópia diferente, e nada na declaração avisa. Em Go, []T versus [3]T
é visível no tipo.
Duas pegadinhas específicas de Go:
append não cresce o slice de quem chamou — nem quando não realoca. len faz
parte do header, e o header foi copiado.
Receiver por valor copia o receiver. Se o tipo tem um sync.Mutex, você copiou o
mutex e o lock não protege nada. go vet acusa: passes lock by value.
Respostas às perguntas-guia
1. Sua linguagem passa argumentos por valor, por referência, ou por valor-da-referência?
Conceito: três comportamentos possíveis, e o terceiro (ponteiro por valor, ou call by sharing) é o mais comum e o mais mal nomeado.
Em Go: sempre por valor — inclusive os ponteiros. Ter *T e &x não é ter
passagem por referência: o *T é copiado como qualquer outro valor de 8 bytes. Slice,
map e chan são valores que contêm ponteiro; "por valor da referência" descreve bem
o efeito, mas o mecanismo é um só: copiar os bytes da representação.
2. Depois de f(x), em que casos x mudou para quem chamou?
Conceito: quando f teve acesso ao armazenamento de x (passagem por
referência), ou quando f seguiu uma referência que estava dentro de x e mutou
o referente. Nunca por reatribuir o parâmetro.
Em Go, mudou quando:
frecebeu*Te escreveu em*pxé slice efescreveu em elemento existente (s[0] = ...)xé map ou chanxé struct que contém ponteiro/slice/map efseguiu o ponteiro
Nunca por append, e nunca por reatribuição de x dentro de f.
3. Por que a = b às vezes copia e às vezes aliasa?
Conceito: sempre copia. Copia a representação. Quando a representação é uma referência, copiar a referência te dá alias do referente. A pergunta certa nunca é "copia?", é "copia o quê?".
Em Go, desenvolvido em 5. Entregável - Por que a = b às vezes copia — inclusive a tabela de tamanhos, que é literalmente a resposta.
4. Como você provaria isso com um teste em 5 linhas?
Conceito: um teste que separa mutar o referente de religar a variável. Se o primeiro é visível e o segundo não, você tem ponteiro por valor.
Em Go:
func TestAliasing(t *testing.T) {
s := []int{1, 2, 3}
muta(s) // s[0] = 99 -> muta o referente
acrescenta(s) // s = append(...) -> religa a variável local
if s[0] != 99 || len(s) != 3 {
t.Fatalf("esperado [99 2 3] len 3, veio %v len %d", s, len(s))
}
}
O teste passa: o elemento mudou, o comprimento não. É a prova em duas linhas.
Trade-offs
Do conceito:
- Por valor é fácil de raciocinar (nenhuma ação a distância) e custa a cópia.
- Por referência evita cópia e permite funções que religam variáveis do chamador — poder que quase sempre produz código difícil de seguir.
- Ponteiro por valor é o meio-termo que todas as linguagens gerenciadas escolheram: barato, permite mutação compartilhada intencional, e cobra o preço de ninguém entender direito o que está acontecendo.
- Cópia rasa universal é a decisão certa por performance e a origem de uma classe inteira de bugs de aliasing.
Da escolha de Go:
- Uma regra só (sempre valor) é simples; o preço é que a semântica depende do tipo, e a chamada não te diz nada.
- Struct grande por valor custa a cópia; por ponteiro custa possível alocação (2. Stack vs Heap), indireção e nilabilidade.
- Slice header de 24 bytes é baratíssimo de copiar — e é exatamente isso que esconde o aliasing do array de trás.
- Receiver por valor dá imutabilidade local de graça, e é armadilha em qualquer
tipo com mutex, contador ou
atomic.
Exemplo prático
package main
import "fmt"
type Point struct{ X, Y int }
func mutaElemento(s []int) { s[0] = 99 } // VISÍVEL
func acrescenta(s []int) { s = append(s, 4) } // invisível
func trocaStruct(p Point) { p.X = 99 } // invisível
func trocaPonteiro(p *Point) { p.X = 99 } // VISÍVEL
func mutaMapa(m map[string]int) { m["a"] = 99 } // VISÍVEL
func main() {
s := []int{1, 2, 3}
mutaElemento(s)
fmt.Println(s) // [99 2 3] <- mudou
acrescenta(s)
fmt.Println(s, len(s)) // [99 2 3] 3 <- NÃO mudou
p := Point{1, 1}
trocaStruct(p)
fmt.Println(p) // {1 1}
trocaPonteiro(&p)
fmt.Println(p) // {99 1}
m := map[string]int{"a": 1}
mutaMapa(m)
fmt.Println(m) // map[a:99] <- mudou
}
O aliasing que morde de verdade — dois slices, um array:
a := []int{1, 2, 3, 4, 5}
b := a[:2] // len 2, cap 5 — MESMO array de trás
b = append(b, 99) // cap sobra: NÃO realoca, escreve em a[2]
fmt.Println(a) // [1 2 99 4 5] <- a mudou sem ninguém tocar em a
fmt.Println(b) // [1 2 99]
O mesmo append que era invisível na função é destrutivo aqui. A diferença é cap.
Se você quer garantia de cópia: b := slices.Clone(a[:2]), ou o idioma
a[:2:2] (limita cap, forçando o próximo append a realocar).
O mutex copiado:
import "sync"
type Contador struct {
mu sync.Mutex
n int
}
func (c Contador) IncErrado() { c.mu.Lock(); defer c.mu.Unlock(); c.n++ } // copia tudo
func (c *Contador) IncCerto() { c.mu.Lock(); defer c.mu.Unlock(); c.n++ }
IncErrado não incrementa e não protege — go vet acusa
IncErrado passes lock by value. Regra prática: se o tipo tem estado mutável,
todos os métodos usam receiver ponteiro.
Relacionado
- 5. Entregável - Por que a = b às vezes copia — a página desta semana
- 4. Mutabilidade — aliasing só é problema porque existe mutação
- 2. Stack vs Heap — ponteiro em parâmetro frequentemente força escape
- 2. Array e 6. Hash Map — semana 2, o que existe atrás do header
Parte de Semana 01 - Diagnóstico e o que Acontece por Baixo · 00 - MOC Fundamentos de Programação