← Painel 60 min
Primeiros passos

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.

VISUALIZAÇÃOVisualização passo a passo
1 / 5
var count int reserva a caixa já gravada com o zero value 0 — nunca lixo de memória Visualizacao deterministica do tipo stack memória de main — uma caixa por variável (a mais recente em destaque)count intvalor=0 var count int reserva a caixa já gravada com o zero value 0 — nunca lixo de memória

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, nunca undefined. Comparar com o zero value é o idioma de “campo não preenchido”.
  • var name tipo declara 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, a troca 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.

Essa lição ficou clara?