Fundamentos de Programação/01 - Fundamentos Duros/Semana 01 - Diagnóstico e o que Acontece por Baixo7 min
Mutabilidade
- Quais tipos da sua linguagem são imutáveis por natureza?
- Que classe de bug a imutabilidade elimina — e qual custo ela cobra?
- O que acontece quando um objeto mutável é chave de hash ou default de função?
- Onde no seu código atual uma cópia defensiva resolveria um bug latente?
Conceito
Um valor é mutável quando seu estado pode mudar depois de criado, no lugar, de forma observável por qualquer alias que aponte para ele.
Mutação sozinha é inofensiva. Aliasing sozinho é inofensivo. O problema é a soma dos dois.
Se ninguém mais aponta pro valor, mutar é seguro. Se o valor nunca muda, dez mil aliases são seguros. Ação a distância só existe quando alguém muta algo que outro alguém está olhando.
Isso reformula o que imutabilidade faz: ela não elimina aliasing — ela torna aliasing seguro.
O que imutabilidade compra:
- compartilhar sem copiar e sem travar. Se ninguém pode mutar, ninguém precisa de cópia defensiva nem de lock. É por isso que linguagens funcionais lidam bem com concorrência: o problema não é resolvido, é inexistente.
- identidade estável. Chave de hash, cache e conjunto exigem que o valor não mude depois de indexado — mutar uma chave depois de inserida corrompe a estrutura de forma silenciosa.
- raciocínio local. O valor que você leu continua sendo o valor que você leu.
- snapshot barato via estruturas persistentes com compartilhamento estrutural.
O que ela cobra: toda "modificação" aloca. Construir um valor incrementalmente com operações imutáveis ingênuas é quadrático — cada passo copia o acumulado.
O espectro de suporte das linguagens (importa mais que a pergunta binária):
| Nível | O que significa | Exemplos |
|---|---|---|
| Imutabilidade profunda imposta | mutação simplesmente não existe | Haskell, Clojure, Erlang |
| Imutabilidade rasa opt-in | você marca; o compilador cobra | Rust (& vs &mut), C++ const |
| Semântica de valor | atribuição copia; limita o aliasing em vez de proibir mutação | C, Go (structs), Swift |
| Convenção | a linguagem não ajuda | Go (no geral), Java, Python |
Note que semântica de valor não é imutabilidade. Ela reduz aliasing — e o original continua mutável por quem o tiver.
Onde a mutabilidade morde, sempre nos mesmos quatro lugares: chave de hash, valor default compartilhado, cache/igualdade, e concorrência.
A contramedida universal: cópia defensiva. Duas fronteiras, e a maioria dos projetos cuida só de uma:
- saída — o que você devolve para fora
- entrada — o que você aceita e guarda
Em Go
Go fica quase todo na última linha da tabela: convenção.
O que é imutável de fato:
string—s[0] = 'x'não compila. Fatiar é O(1) e compartilha o array de bytes.const— e só para tipos básicos: bool, string, numéricos. Não existeconstde struct, slice ou map.- E acabou. Não há
readonly,final,frozen, nem struct imutável de verdade.
O que Go dá em troca: semântica de valor. Struct e array são copiados na atribuição e na passagem, o que dá imutabilidade efetiva enquanto ninguém tiver ponteiro. É a linha 3 da tabela, e é genuinamente útil — mas é uma garantia mais fraca do que parece.
O buraco clássico: struct "imutável" com campo slice ou map. O campo é um header/ponteiro; a cópia da struct compartilha os dados de trás. Cópia rasa, sempre.
Onde Go ajuda mais que a média: o compilador proíbe estruturalmente chave de mapa não comparável. Slice, map e func não compilam como chave. Isso mata na compilação um bug que Java e Python só descobrem em produção.
Onde ele não ajuda:
- struct com ponteiro compila como chave e compara pelo ponteiro
map[any]Vcompila e dá panic em runtime com chave não comparáveltime.Timecomo chave é armadilha:==compara wall clock, monotonic e location. Use.Equalou normalize- array é chave válida (
[2]intsim,[]intnão)
O equivalente ao "default mutável compartilhado": Go não tem argumento default, mas
tinha a variável de loop capturada — corrigido no Go 1.22, quando cada iteração
passou a ganhar variável nova. Antes disso todo
for _, v := range xs { go func(){ use(v) }() } era bug.
Ferramentas de cópia defensiva: slices.Clone e maps.Clone, stdlib desde o
Go 1.21. Ambas rasas.
Respostas às perguntas-guia
1. Quais tipos da sua linguagem são imutáveis por natureza?
Conceito: a pergunta relevante não é quais tipos, é em que nível do espectro a linguagem está — imposta, opt-in, semântica de valor, ou convenção.
Em Go: só string (e constantes, que não são valores em runtime). Nada mais. Slice,
map e chan já são referências mutáveis compartilhadas.
2. Que classe de bug a imutabilidade elimina — e qual custo ela cobra?
Conceito: elimina mutação a distância — o dado que você guardou muda porque alguém tinha alias dele. Cobra alocação em toda modificação, e construção ingênua vira O(n²).
Em Go, a mutação a distância aparece em dois lugares muito específicos: o getter que
devolve o slice interno, e o construtor que guarda o slice recebido de fora. O custo
aparece na concatenação de string em loop — cada += aloca uma string nova. Use
strings.Builder.
3. O que acontece quando um objeto mutável é chave de hash ou default de função?
Conceito: chave de hash — o hash é calculado na inserção. Se a chave muda depois, o valor fica num bucket onde ninguém mais o procura: perdido, sem erro. Default compartilhado — o valor é criado uma vez e reusado entre chamadas, então uma mutação vaza de uma invocação para a seguinte.
Em Go, o compilador te salva do primeiro caso (slice/map/func não compilam como
chave) e não te salva de struct-com-ponteiro, de map[any], nem de time.Time.
Default de função não existe; o equivalente era a variável de loop, resolvido no 1.22.
4. Onde no seu código atual uma cópia defensiva resolveria um bug latente?
Conceito: nas duas fronteiras do tipo — o que sai e o que entra. A maioria protege só a saída.
Em Go: slices.Clone no getter que devolve []T, e clone antes de guardar um slice
recebido de fora no construtor ou setter.
Trade-offs
Do conceito:
- Imutabilidade imposta elimina a classe de bug e te obriga a estruturas persistentes, que têm constante pior e complexidade conceitual maior.
- Opt-in verificado (Rust) dá o melhor dos dois e cobra em curva de aprendizado — o compilador passa a discutir com você.
- Semântica de valor é barata e dá garantia rasa, o que é uma armadilha justamente porque parece garantia total.
- Convenção custa zero e falha exatamente quando o time cresce.
Da escolha de Go:
- String imutável dá slicing O(1) e chave de mapa segura; cobra alocação em toda modificação.
- Semântica de valor evita aliasing sem depender do GC; copiar struct grande custa, e a cópia é sempre rasa.
- Cópia defensiva é decisão, não regra: ela custa alocação em caminho quente. Quando você escolher não copiar, documente o contrato ("o slice devolvido é interno, não modifique").
- Receiver por valor parece imutabilidade e não é: protege os campos escalares, não o que está atrás dos ponteiros.
Exemplo prático
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
type Config struct {
nome string
tags []string // <- o furo na "imutabilidade" da struct
}
func (c Config) Tags() []string { return c.tags } // vaza o interno
func (c Config) TagsSeguro() []string { return slices.Clone(c.tags) } // cópia defensiva
func main() {
c := Config{nome: "prod", tags: []string{"a", "b"}}
d := c // "cópia" da struct...
d.Tags()[0] = "X" // ...mas o array de trás é compartilhado
fmt.Println(c.tags) // [X b] <- o "original" mutou
c2 := Config{nome: "prod", tags: []string{"a", "b"}}
c2.TagsSeguro()[0] = "X"
fmt.Println(c2.tags) // [a b] <- protegido
// string é imutável:
s := "olá"
// s[0] = 'O' // erro: cannot assign to s[0] (neither addressable nor a map index expression)
s = strings.ToUpper(s) // gera string nova
fmt.Println(s)
// concat em loop é O(n^2); Builder é O(n):
var b strings.Builder
for i := 0; i < 3; i++ {
b.WriteString("x")
}
fmt.Println(b.String())
m := map[string]int{"a": 1}
m2 := maps.Clone(m) // cópia RASA (Go 1.21+)
m2["a"] = 2
fmt.Println(m["a"], m2["a"]) // 1 2
}
O que o compilador deixa passar como chave:
type Chave struct{ p *int }
func main() {
x, y := 1, 1
m := map[Chave]string{}
m[Chave{&x}] = "primeiro"
m[Chave{&y}] = "segundo"
fmt.Println(len(m)) // 2 — mesmo valor apontado, ponteiros diferentes
// _ = map[[]int]string{} // erro de compilação: invalid map key type []int
qualquer := map[any]string{}
qualquer[[]int{1}] = "x" // COMPILA e dá panic: hash of unhashable type []int
}
Relacionado
- 3. Passagem por Valor vs por Referência — aliasing é a outra metade do problema
- 5. Entregável - Por que a = b às vezes copia — a página desta semana
- 2. Stack vs Heap — cópia defensiva = alocação = pressão de GC
- 6. Hash Map — semana 2, por que a chave precisa ter hash estável
- 1. Encapsulamento — semana 5, o getter que vaza o interno é falha de encapsulamento
Parte de Semana 01 - Diagnóstico e o que Acontece por Baixo · 00 - MOC Fundamentos de Programação