Variáveis: var, := e o fim do undefined
Pré-requisitos: 03-valores-e-tipos
Intuição
Todo hotel decente tem uma regra simples: nenhum hóspede entra em um quarto no estado em que o anterior deixou. Entre uma estadia e outra, o quarto volta a um estado conhecido — cama feita, lixo vazio, frigobar reposto. Você pode não gostar da decoração, mas nunca vai abrir a porta e encontrar uma surpresa.
Memória de computador funciona como esses quartos: o espaço que seu programa recebe já foi usado por outros programas, e o que ficou lá é literalmente o lixo do hóspede anterior. Linguagens como C entregam o quarto sujo — uma variável não inicializada contém bytes aleatórios, e ler dela é um bug clássico e difícil de rastrear, porque o valor muda a cada execução. JavaScript entrega um quarto com uma placa estranha: undefined, um “valor” que se espalha pelo programa e só explode quando alguém tenta usá-lo de verdade, três funções depois. Python nem deixa você perguntar: referenciar variável não atribuída lança exceção em runtime.
Go escolheu a regra do hotel: toda variável nasce em um estado conhecido e útil. Declarou var count int sem atribuir nada? count vale 0. Uma string nasce "", um bool nasce false, um float64 nasce 0. Esse valor inicial garantido tem nome — zero value — e não é um detalhe: é uma decisão de design que elimina simultaneamente o lixo de memória do C e o undefined do JavaScript. Não existe, em Go, o estado “declarada mas sem valor”. Consequência prática imediata: você pode declarar uma variável e usá-la na linha seguinte, sem cerimônia de inicialização, e o comportamento é o mesmo em toda execução, em toda máquina.
Com a inicialização garantida, sobra a segunda pergunta desta aula: como declarar. Go tem duas formas, e a escolha entre elas comunica intenção. A forma completa, var count int, diz “quero esta variável com seu zero value” — ou, com valor, var count int = 10. E a forma curta, count := 10, diz “crie e inicialize com este valor, e o tipo você deduz”. O := é o que você mais vai digitar em Go — dentro de funções, é a forma dominante no código idiomático — mas cada uma tem seu território, e o Jeito Go desta aula fecha essa fronteira.
Repare no padrão que se forma entre as aulas: tipos que não se misturam (aula 3), variáveis que nunca ficam indefinidas e código morto que não compila (esta aula). Go remove sistematicamente os estados intermediários vagos onde bugs moram. O que sobra é um programa em que toda variável, em qualquer ponto, tem um tipo conhecido e um valor conhecido — e essa é a fundação sobre a qual o restante do curso constrói.
Código anotado
O programa desta aula percorre as duas formas de declaração e as regras que as acompanham — inclusive as duas que geram erro de compilação, que ficam indicadas em comentários para você reproduzir no Experimente.
Começamos com a forma completa. var + nome + tipo, sem valor: a variável nasce com o zero value do tipo. As quatro primeiras linhas do main declaram uma de cada tipo básico e nenhuma recebe atribuição — e ainda assim todas têm valor imprimível.
package main
import "fmt"
func main() {
var count int
var price float64
var name string
var active bool
fmt.Printf("count=%d price=%g name=%q active=%t\n",
count, price, name, active)A forma curta := declara e inicializa em um passo, e o tipo vem do valor: 10 é int, "Gopher" é string, 4990 * 3 é int porque a conta é entre inteiros. É a forma dominante dentro de funções — menos digitação sem perder nenhuma garantia, porque o tipo continua fixo: tentar mascot = 10 depois seria o erro de tipos da aula 3.
quantity := 3
mascot := "Gopher"
totalCents := 4990 * quantity
fmt.Println(quantity, mascot, totalCents)Atribuição múltipla: os valores do lado direito são todos avaliados antes de qualquer gravação no lado esquerdo. É por isso que x, y = y, x troca os dois sem variável temporária — quando a gravação começa, os valores antigos já foram lidos. Você vai usar exatamente isso no exercício 1.
x, y := 1, 2
x, y = y, x
fmt.Println("depois da troca:", x, y)As regras de erro, para conhecer de propósito. Primeira: := exige ao menos uma variável nova do lado esquerdo — reusar quantity := 5 no mesmo bloco não compila (no new variables on left side of :=); para reatribuir, use =. Segunda: variável declarada e não usada não compila (declared and not used). A linha comentada abaixo, se descomentada, derruba o build.
quantity = 5 // reatribuição usa =, não :=
// var unused int // descomente e o programa não compila
fmt.Println("nova quantidade:", quantity)
}O programa completo compila e roda. Guarde a assimetria que o chunk 4 mostra: := cria, = atualiza. A mensagem de erro de cada um aponta o outro — o compilador de Go é didático nesse ponto, e ler essas mensagens uma vez evita confusão para sempre.
O carrossel abaixo mostra a história desta aula do ponto de vista da memória: cada declaração reserva uma “caixa” que já nasce preenchida com o zero value do tipo, atribuições gravam por cima sem trocar a caixa, e o := faz os dois movimentos de uma vez.
Experimente
Preveja a saída das três primeiras linhas antes de rodar — os zero values do carrossel são exatamente o que deve aparecer.
package main
import "fmt"
func main() {
var visits int
var username string
var isAdmin bool
fmt.Println("visitas:", visits)
fmt.Printf("usuário: %q\n", username)
fmt.Println("admin:", isAdmin)
visits = visits + 1
score := visits * 100
fmt.Println("depois do incremento:", visits, "score:", score)
}Agora provoque os dois erros de compilação da aula: troque visits = visits + 1 por visits := visits + 1 e leia a mensagem; depois desfaça e adicione var ghost string sem usar ghost em lugar nenhum. Por fim, um caso que funciona e surpreende: mude a última linha para dentro de um novo bloco { score := 999; fmt.Println(score) } e imprima score de novo fora do bloco — são duas variáveis diferentes, uma sombreando a outra. Esse fenômeno (shadowing) volta a aparecer na aula de if com init statement.
Exercício 1
Troque dois valores
Implemente Swap(a int, b int) (int, int), que devolve os dois valores recebidos em ordem invertida.
Entrada: dois inteiros quaisquer. Saída: o par (b, a).
Exemplos:
Swap(1, 2)→(2, 1);Swap(-5, 10)→(10, -5);Swap(7, 7)→(7, 7).
O exercício é pequeno de propósito: ele existe para você usar a atribuição múltipla do chunk 3 e entender por que ela dispensa a variável temporária que outras linguagens exigem. Resolva usando a, b = b, a — e depois, se quiser, pense em qual seria a versão sem ela.
Solução & Explicação
func Swap(a int, b int) (int, int) {
a, b = b, a
return a, b
}A garantia que faz isso funcionar: na atribuição múltipla, todo o lado direito é avaliado antes da primeira gravação. b e a são lidos, e só então gravados em a e b. Custo O(1). Parâmetros de função são variáveis locais comuns em Go — reatribuí-los não afeta o chamador, então usar a e b como espaço de trabalho é legítimo. (Há uma versão ainda mais curta, return b, a, igualmente correta — o objetivo pedagógico aqui era a atribuição múltipla, que você vai reencontrar em contextos onde o return direto não existe, como trocar elementos de um slice: values[i], values[j] = values[j], values[i].)
A armadilha é a versão ingênua em duas linhas: a = b seguida de b = a. Depois da primeira linha, o valor original de a não existe mais — a segunda linha copia b para b, e o resultado é (2, 2) em vez de (2, 1). O caso de teste Swap(7, 7) é o único que essa versão passa por coincidência; os outros três a reprovam. Em linguagens sem atribuição múltipla, a correção exige temp := a antes — em Go, esse temp é ruído.
Exercício 2
Preencha os padrões sobre zero values
Um formulário de cadastro chega com campos opcionais, e campo não preenchido chega como o zero value do tipo. Implemente FillDefaults(name string, quantity int) (string, int) que aplica os padrões: se name vier "", use "sem nome"; se quantity vier 0, use 1.
Entrada: um nome (possivelmente vazio) e uma quantidade (possivelmente zero, nunca negativa). Saída: o par com os padrões aplicados — cada campo decidido independentemente.
Exemplos:
FillDefaults("", 0)→("sem nome", 1);FillDefaults("Caneca", 3)→("Caneca", 3);FillDefaults("", 5)→("sem nome", 5)— só o nome estava vazio;FillDefaults("Camiseta", 0)→("Camiseta", 1)— só a quantidade.
Os casos 3 e 4 são o coração do exercício: cada campo tem sua própria decisão, e um vir preenchido não diz nada sobre o outro.
Solução & Explicação
Duas decisões independentes, dois ifs independentes:
func FillDefaults(name string, quantity int) (string, int) {
if name == "" {
name = "sem nome"
}
if quantity == 0 {
quantity = 1
}
return name, quantity
}Comparar com o zero value do tipo — "" para string, 0 para int — é o idioma padrão de “este campo não foi preenchido” em Go, e você o verá em código de verdade constantemente (é também a base do comportamento de structs e JSON mais adiante no curso). Reatribuir os parâmetros evita declarar variáveis novas só para carregar os mesmos dados. Custo O(1).
A armadilha estrutural é acoplar as decisões: if name == "" && quantity == 0 { ... } else { ... } trata “ambos vazios” e “ambos preenchidos”, mas escolhe errado nos casos mistos 3 e 4 — exatamente os que o harness cobra. Quando duas decisões são independentes no enunciado, mantê-las independentes no código não é só correção: é o que impede a explosão de ramos (2 campos acoplados = 4 combinações; 5 campos = 32). Vale registrar a limitação honesta do idioma: ele não distingue “usuário digitou 0” de “usuário não preencheu”. Aqui o contrato diz que 0 significa não preenchido, então está correto; quando o seu domínio precisar da distinção, a ferramenta muda (ponteiros, aula 15).
Exercício 3
Monte o resumo do pedido
Combine as três aulas: declaração com :=, aritmética inteira de dinheiro e formatação. Implemente OrderSummary(product string, quantity int, unitCents int) string que monta a linha de resumo de um item do pedido, com o total calculado.
Entrada: o nome do produto, a quantidade (maior que zero) e o preço unitário em centavos. Saída: a string "<quantity>x <product> = R$ <reais>.<centavos>", com o total quantity * unitCents exibido em reais e os centavos sempre com dois dígitos.
Exemplos:
OrderSummary("Camiseta", 3, 4990)→"3x Camiseta = R$ 149.70";OrderSummary("Adesivo", 10, 305)→"10x Adesivo = R$ 30.50";OrderSummary("Curso", 2, 7450)→"2x Curso = R$ 149.00"— repare no.00: total redondo ainda exibe os dois dígitos.
Sugestão de estrutura: uma variável para o total em centavos, e a decomposição /100 e %100 da aula 3 sobre ela.
Solução & Explicação
func OrderSummary(product string, quantity int, unitCents int) string {
totalCents := quantity * unitCents
return fmt.Sprintf("%dx %s = R$ %d.%02d", quantity, product, totalCents/100, totalCents%100)
}A variável intermediária totalCents é a decisão de estilo que importa aqui. Sem ela, a expressão quantity*unitCents apareceria duas vezes dentro do Sprintf — uma para /100, outra para %100 — e toda duplicação de expressão é um lugar onde uma futura edição corrige um lado e esquece o outro. Com ela, a conta tem nome, acontece uma vez e o Sprintf lê como a etiqueta que produz. O tipo de totalCents é int, inferido da multiplicação entre inteiros — nenhuma anotação necessária. Custo O(n) no tamanho da string.
As armadilhas são reprises das aulas anteriores em combinação: multiplicar em float (float64(quantity) * float64(unitCents) / 100) reintroduz o arredondamento binário que centavos inteiros existem para evitar; e usar %d em vez de %02d nos centavos passa nos casos 1 e 3 mas falha no caso 4 (R$ 149.0 em vez de R$ 149.00). Note o padrão dos exercícios até aqui: os casos de teste são escolhidos para reprovar as soluções quase certas — leia sempre todos os casos antes de codificar, como você faria com uma especificação.
Resumo
- Toda variável em Go nasce com o zero value do seu tipo:
0,"",false— nunca lixo de memória, nuncaundefined. Comparar com o zero value é o idioma de “campo não preenchido”. var name tipodeclara com zero value;:=declara e inicializa com tipo inferido — e é a forma dominante dentro de funções.:=cria,=atualiza: reusar:=na mesma variável ou esquecer o=na reatribuição são erros de compilação com mensagens que apontam um para o outro.- Atribuição múltipla avalia todo o lado direito antes de gravar:
a, b = b, atroca valores sem temporária. - Variável declarada e não usada não compila — mesma política do import não usado: código morto não entra.
Na próxima aula, imersao/05-constantes-e-iota, você conhece o lado imutável das declarações: const, os números “sem tipo” que quebram uma regra da aula 3 de um jeito controlado, e o iota que gera sequências de constantes sozinho.