trilha

Fundamentos de Programação/01 - Fundamentos Duros/Semana 01 - Diagnóstico e o que Acontece por Baixo5 minentregavel

Entregável - Por que a = b às vezes copia

Por que a = b às vezes copia e às vezes não

Tese

a = b sempre copia — em Go e em qualquer linguagem imperativa. A pergunta errada é "copia ou não copia"; a pergunta certa é "copia o quê?".

O conceito, antes do Go

Atribuição copia a representação do valor. O que varia entre linguagens não é se há cópia — é o que cabe na representação:

  • Se a representação é o dado inteiro (um inteiro, uma struct por valor), copiar dá independência.
  • Se a representação é uma referência ao dado (um ponteiro, um handle), copiar dá alias: dois nomes, um dado.

Nenhuma linguagem mainstream copia profundamente por padrão, e isso não é omissão. Cópia profunda é caríssima e muitas vezes indefinida: o que copiar de um socket aberto? De um grafo com ciclo? De um mutex travado? Então cada linguagem escolhe, tipo por tipo, o que entra na representação — e é essa escolha que decide se a = b isola ou compartilha.

Linguagem A escolha que ela fez Efeito de a = b
Java objeto é sempre referência; primitivo é valor objeto aliasa, primitivo copia — e nada no tipo avisa
Python tudo é referência (imutáveis tornam o alias inofensivo) sempre aliasa
C++ você decide, e pode redefinir com construtor de cópia / operator= pode ser raso ou profundo
C struct copia campo a campo; ponteiro aliasa visível no tipo
Go struct e array copiam; slice, map e chan contêm ponteiro visível no tipo

Então a pergunta "por que às vezes copia" é, no fundo: quais tipos da minha linguagem carregam referência dentro? Em Go a resposta é uma tabela curta — e é o resto desta página.

A regra única, em Go

Todo tipo em Go tem um tamanho fixo, conhecido em tempo de compilação. a = b copia exatamente esses bytes — nem mais, nem menos. Não existe cópia profunda em Go, nem na linguagem nem na biblioteca padrão.

E existe uma função que te diz literalmente quantos bytes a = b copia: unsafe.Sizeof. Ela é a resposta da pergunta, não uma aproximação dela.

O que cabe em cada valor (amd64/arm64)

Tipo unsafe.Sizeof O que está dentro a = b dá alias?
bool 1 o valor não
int, int64, float64 8 o valor não
*T 8 um endereço sim
string 16 ptr + len não (imutável)
[]T 24 ptr + len + cap sim (do array de trás)
map[K]V 8 ptr pro hmap sim
chan T 8 ptr pro hchan sim
func() 8 ptr pro closure sim (das capturas)
any / error 16 ptr de tipo + ptr de dado depende do que tem dentro
struct{A, B int} 16 os dois campos não
[3]int 24 os três elementos não
struct{} 0 nada

Duas linhas dessa tabela explicam quase toda a confusão:

  • [3]int copia 24 bytes de dados. []int copia 24 bytes de metadados. Mesmo tamanho, significados opostos.
  • map é 8 bytes. Não existe "copiar um mapa" em Go — você copia o ponteiro. Para copiar de verdade: maps.Clone.

De onde vem a sensação de que "às vezes não copia"

1. O valor contém ponteiro. Você copiou o ponteiro fielmente; os dois lados passam a olhar o mesmo dado. A cópia funcionou — ela só era rasa.

2. Array vs slice. [3]int é um valor; []int é uma janela para um array que está em outro lugar.

3. Receiver de método. func (c Config) copia o receiver; func (c *Config) não. A mesma chamada c.Metodo() tem semântica diferente conforme a assinatura.

4. append. Ele pode devolver um slice apontando pro mesmo array (se cap sobra) ou pra um array novo (se realocou). O mesmo código é destrutivo ou inofensivo dependendo de um número que você não vê na chamada.

O teste decisivo

Uma pergunta só

"Esse tipo tem ponteiro dentro?" Se sim, a = b te dá alias e você precisa clonar explicitamente para isolar. Se não, a = b te dá independência total.

Programa que prova tudo

package main

import (
	"fmt"
	"maps"
	"slices"
	"unsafe"
)

type Config struct {
	Nome string
	Tags []string
}

func main() {
	// 1. Sizeof é literalmente "quantos bytes a = b copia"
	fmt.Println("int    ", unsafe.Sizeof(int(0)))          // 8
	fmt.Println("string ", unsafe.Sizeof(""))              // 16
	fmt.Println("[]int  ", unsafe.Sizeof([]int(nil)))      // 24
	fmt.Println("[3]int ", unsafe.Sizeof([3]int{}))        // 24
	fmt.Println("map    ", unsafe.Sizeof(map[int]int(nil)))// 8
	fmt.Println("any    ", unsafe.Sizeof(any(nil)))        // 16

	// 2. Array copia dados; slice copia metadados
	arr1 := [3]int{1, 2, 3}
	arr2 := arr1
	arr2[0] = 99
	fmt.Println(arr1, arr2) // [1 2 3] [99 2 3]  <- independentes

	sl1 := []int{1, 2, 3}
	sl2 := sl1
	sl2[0] = 99
	fmt.Println(sl1, sl2) // [99 2 3] [99 2 3]   <- MESMO array

	// 3. Struct: cópia rasa. Escalar isola, ponteiro não.
	c1 := Config{Nome: "prod", Tags: []string{"a", "b"}}
	c2 := c1
	c2.Nome = "dev"      // campo escalar: isolado
	c2.Tags[0] = "X"     // campo slice: compartilhado
	fmt.Println(c1)      // {prod [X b]}
	fmt.Println(c2)      // {dev [X b]}

	// 4. Cópia profunda é manual
	c3 := c1
	c3.Tags = slices.Clone(c1.Tags)
	c3.Tags[0] = "Z"
	fmt.Println(c1.Tags, c3.Tags) // [X b] [Z b]  <- agora sim

	// 5. Map nunca é copiado por atribuição
	m1 := map[string]int{"a": 1}
	m2 := m1
	m2["a"] = 99
	fmt.Println(m1["a"]) // 99
	m3 := maps.Clone(m1)
	m3["a"] = 1
	fmt.Println(m1["a"], m3["a"]) // 99 1

	// 6. append: mesmo array ou array novo, sem aviso
	base := []int{1, 2, 3, 4, 5}
	vista := base[:2]              // len 2, cap 5
	vista = append(vista, 99)      // cap sobra: escreve em base[2]
	fmt.Println(base)              // [1 2 99 4 5]

	base2 := []int{1, 2, 3}
	vista2 := base2[:2]            // len 2, cap 3
	vista2 = append(vista2, 8, 9)  // não cabe: realoca
	fmt.Println(base2, vista2)     // [1 2 3] [1 2 8 9]
}

== segue exatamente a mesma lógica

Comparação em Go opera sobre os mesmos bytes que a atribuição copia:

  • *T == *T compara endereços, não o que está apontado
  • struct compara campo a campo, e só compila se todos os campos forem comparáveis
  • slice não é comparável: a == b em []intinvalid operation: a == b (slice can only be compared to nil)
  • struct contendo slice também não, e a mensagem é outra → invalid operation: a == b (struct containing []int cannot be compared)
  • para comparação profunda: slices.Equal, maps.Equal (Go 1.21+) ou reflect.DeepEqual
  • time.Time com == compara wall clock + monotonic + location — use .Equal

Conclusão em três regras

  1. a = b copia unsafe.Sizeof(b) bytes. Sempre. Se o resultado te surpreende, olhe o que está dentro desses bytes.
  2. Ponteiro dentro do valor = alias. Slice, map, chan, func e interface têm. Isolamento exige clone explícito (slices.Clone, maps.Clone, ou campo a campo).
  3. Cópia profunda não existe em Go. Isso não é omissão: é a linguagem se recusando a esconder uma alocação de você.

Parte de Semana 01 - Diagnóstico e o que Acontece por Baixo · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas