← Painel 60 min
Primeiros passos

Valores e tipos: o que a tipagem estática garante (e o que ela recusa)

Pré-requisitos: 02-hello-world-e-toolchain

Intuição

Em um laboratório de análises, cada amostra ganha uma etiqueta na entrada: sangue, urina, tecido. A etiqueta não é burocracia — é o que impede a centrífuga de sangue de receber uma amostra de tecido e produzir um resultado sem sentido com cara de resultado válido. Ninguém “descobre” o tipo da amostra na hora do exame; ele foi declarado na entrada e conferido em cada etapa.

Tipos em Go funcionam assim. Todo valor tem um tipo conhecido em tempo de compilaçãoint, float64, string, bool — e toda operação declara quais tipos aceita. Quando os tipos não batem, o programa não compila. Compare com o outro modelo: em JavaScript, "10" - 1 vale 9, mas "10" + 1 vale "101" — o interpretador adivinha uma conversão, e cada adivinhação é uma chance de o exame errado rodar sem ninguém notar. A postura de Go é a do laboratório: nada se converte sozinho. Se você quer tratar um int como float64, escreve float64(x) — e essa conversão explícita é o registro visível de que a mudança de tipo foi intencional.

O que você ganha em troca dessa cerimônia é uma categoria inteira de bugs que deixa de existir. O NaN que atravessa dez funções até explodir num relatório, o "undefined" concatenado numa URL, o campo que era número e virou string no caminho — nada disso sobrevive à compilação em Go. O compilador vira um revisor que confere cada fronteira de tipo do programa inteiro, em milissegundos, antes de qualquer execução.

Nesta aula você conhece os quatro tipos que carregam a maior parte de qualquer programa. int para números inteiros — contagens, índices e, como você vai ver nos exercícios, dinheiro em centavos. float64 para medidas com fração — mas com uma ressalva importante sobre precisão que custa dinheiro real a quem ignora. string para texto: uma sequência imutável de bytes, por convenção em UTF-8 — imutável significa que nenhuma operação altera uma string existente; operações como a concatenação que você já usou criam sempre uma string nova. E bool para true e false — e nada além deles: em Go, if 1 {} não compila, porque inteiro não é booleano nem “verdadeiroso”.

Os booleanos merecem um parágrafo próprio, porque é deles que sai o fluxo de controle das próximas aulas. Valores bool nascem de três fontes: dos literais true e false, de comparações (price > 4000, name == "Maria", cents != 0) e dos operadores lógicos && (e), || (ou) e ! (não). Uma comparação em Go é uma expressão comum que produz um bool — você pode guardá-la em uma variável (isExpensive := price > 4000), passá-la para uma função ou devolvê-la de uma, exatamente como faria com um número. E as comparações também obedecem à regra dos tipos: comparar int com float64 sem conversão é o mesmo erro de compilação que somá-los.

Uma pergunta justa neste ponto: se os tipos são tão rigorosos, como um int vira texto para aparecer em uma etiqueta, e como o texto digitado por um usuário vira número? A resposta tem duas partes. Para exibição, o pacote fmt que você já usa formata qualquer tipo com os verbos %d, %s, %f — é conversão para texto, mas declarada no template, visível. Para o caminho inverso — "1990" virar 1990 — existe o pacote strconv, que devolve dois valores: o número e um erro, porque nem toda string é um número válido. Você vai encontrá-lo na aula de stdlib; por enquanto, o que importa é o princípio: a fronteira entre texto e número em Go é sempre atravessada por uma função explícita que pode falhar, nunca por coerção automática que “dá um jeito”.

A regra da conversão explícita vai aparecer três vezes nos exercícios de hoje, cada vez com uma consequência diferente: uma divisão que descarta centavos, uma divisão que os preserva e uma formatação que decide entre os dois caminhos. No fim da aula, “que tipo é esse valor?” terá virado a primeira pergunta que você faz ao ler qualquer linha de Go — que é exatamente o hábito que a linguagem quer instalar.

Código anotado

O programa a seguir apresenta os quatro tipos em um cenário concreto — os dados de um pedido em uma loja — e mostra as duas regras que causam surpresa em quem chega: divisão inteira e conversão obrigatória.

Além de fmt, nada de novo nos imports. As declarações usam var nome tipo = valor — a forma completa, que deixa o tipo visível para fins didáticos. Na próxima aula você verá a forma curta que o Go infere sozinho.

package main

import "fmt"

Quatro valores, quatro tipos. itemCents guarda o preço em centavos, como inteiro — convenção universal em sistemas de pagamento, pela razão que o chunk 4 demonstra. fmt.Printf com o verbo %T imprime o tipo de um valor: ferramenta de investigação que vale conhecer cedo.

func main() {
	var itemName string = "Camiseta Gopher"
	var itemCents int = 4990
	var weightKg float64 = 0.25
	var inStock bool = true

	fmt.Printf("%v é %T\n", itemName, itemName)
	fmt.Printf("%v é %T\n", itemCents, itemCents)
	fmt.Printf("%v é %T\n", weightKg, weightKg)
	fmt.Printf("%v é %T\n", inStock, inStock)

Primeira regra: divisão entre inteiros produz inteiro, descartando a fração — e o operador % entrega o resto que a divisão descartou. Juntos, eles decompõem qualquer valor em centavos: 4990/10049 reais e 4990%10090 centavos. Nenhum float envolvido, nenhum arredondamento possível.

	reais := itemCents / 100
	cents := itemCents % 100
	fmt.Printf("preço: R$ %d.%02d\n", reais, cents)

Segunda regra: tipos diferentes não se misturam sem conversão. Somar o peso (float64) com a quantidade (int) direto não compila; float64(quantity) cria um novo valor do tipo certo e a soma passa a ser entre iguais. A conversão é sempre visível no código — é o “assinei embaixo” da mudança de tipo.

	quantity := 3
	totalWeight := weightKg * float64(quantity)
	fmt.Println("peso total do pacote:", totalWeight, "kg")

E o motivo de dinheiro ser int: floats de 64 bits são frações binárias, e 0.1 ou 0.2 não têm representação binária exata — assim como 1/3 não tem representação decimal exata. O erro é minúsculo, mas existe, e a comparação com 0.3 falha. Em centavos inteiros, 10+20 == 30 sem ressalva.

	fmt.Println("0.1 + 0.2 == 0.3?", 0.1+0.2 == 0.3)
	fmt.Println("0.1 + 0.2 =", 0.1+0.2)
	fmt.Println("em centavos:", 10+20 == 30)
}

Rode o programa completo e leia a saída com calma: o 0.1 + 0.2 imprime 0.30000000000000004. Isso não é bug de Go — é o padrão IEEE 754, o mesmo em JavaScript, Python e Java. A diferença é que Go te dá int de verdade para os casos em que fração binária é inaceitável.

Dois esclarecimentos sobre os inteiros antes dos exercícios. Primeiro, o tamanho: int ocupa 64 bits nas plataformas atuais, o que cobre valores até cerca de 9 quintilhões — para contagens, índices e dinheiro em centavos, o overflow não é uma preocupação do dia a dia, mas saiba que ele existe: um int que estoura o limite dá a volta silenciosamente, sem erro em runtime. Segundo, a família: Go também tem int8, int16, int32, int64 e as variantes sem sinal uint.... Você não precisa deles agora — a regra prática, que o Jeito Go desta aula detalha, é usar int puro até que um contrato externo exija um tamanho específico. E note que a regra da não mistura vale dentro da família: int32 + int64 não compila, mesmo sendo ambos inteiros.

Experimente

Três previsões antes de rodar: qual o valor de 7 / 2? E de 7.0 / 2? E a linha do float64(7) / 2 imprime o mesmo que qual das duas anteriores?

package main

import "fmt"

func main() {
	fmt.Println("7 / 2 =", 7/2)
	fmt.Println("7 % 2 =", 7%2)
	fmt.Println("7.0 / 2 =", 7.0/2)
	fmt.Println("float64(7) / 2 =", float64(7)/2)

	var price int = 4990
	fmt.Println("preço em reais:", price/100)
}

Agora quebre o programa de propósito, um erro por vez, e leia cada mensagem: troque a última linha por price / 100.5 (int e float não se misturam); tente var flag bool = 1 (inteiro não é booleano); e escreva if price { fmt.Println("caro") } (condição exige bool — compare com price > 4000). Essas três mensagens de erro são o compilador aplicando exatamente as regras desta aula. Para fechar, adicione fmt.Println(price > 4000 && price < 10000) e confira que a expressão inteira imprime um booleano — comparações são valores como quaisquer outros.

Exercício 1

Converta centavos para reais

O sistema guarda preços em centavos (int), mas o relatório de vendas exibe reais com fração. Implemente CentsToReais(cents int) float64 que converte um valor em centavos para o valor correspondente em reais.

Entrada: um valor não negativo em centavos. Saída: o valor em reais como float64.

Exemplos:

  • CentsToReais(1990)19.9;
  • CentsToReais(1)0.01;
  • CentsToReais(0)0.

Parece uma divisão por 100 — e é. A questão do exercício é onde acontece a conversão de tipo. Se seu resultado para 1990 está dando 19 em vez de 19.9, você encontrou a armadilha; releia o chunk 3 do código anotado.

Solução & Explicação

Converta primeiro, divida depois:

func CentsToReais(cents int) float64 {
	return float64(cents) / 100
}

Com float64(cents) à esquerda, o 100 literal é tratado como float64 também, e a divisão preserva a fração. Custo O(1), sem casos de borda — zero e valores de um dígito passam pela mesma expressão.

A alternativa errada é sutil porque compila: float64(cents / 100). A conversão está lá, mas chega tarde — cents / 100 executa primeiro, entre inteiros, e descarta os centavos; converter 19 para float64 depois só produz 19.0. Os testes deste exercício pegam a diferença no caso 1 (19 contra 19.9) e ainda mais claramente no caso 3, em que float64(1/100) devolve 0 em vez de 0.01. A ordem das operações decide o resultado: converta o operando, não o resultado. Esse é um padrão geral com conversões numéricas — pergunte sempre “em que tipo essa conta está sendo feita?”, não “o resultado final tem o tipo certo?”. E vale a nota de projeto: esta função existe para exibição. Para guardar, somar e comparar dinheiro, os centavos inteiros continuam sendo a representação correta, como o exercício 2 explora.

Exercício 2

Divida a conta sem perder centavo

Quatro pessoas jantaram e a conta veio em centavos. Implemente SplitBill(totalCents int, people int) (int, int) que devolve dois valores: quanto cada pessoa paga (parte inteira, em centavos) e quantos centavos sobram sem dono.

Entrada: o total em centavos (não negativo) e o número de pessoas (sempre maior que zero). Saída: o par (parte, sobra), ambos em centavos, tal que parte*people + sobra == totalCents e sobra < people.

Exemplos:

  • SplitBill(1000, 3)(333, 1) — R$ 3,33 para cada, 1 centavo sobrando;
  • SplitBill(1000, 4)(250, 0);
  • SplitBill(7, 10)(0, 7) — conta menor que o grupo: ninguém paga, sobram 7.

Devolver dois valores é novidade — em Go isso é nativo: return a, b com a assinatura (int, int). A divisão inteira que era armadilha no exercício 1 é a ferramenta certa aqui.

Solução & Explicação

O par quociente/resto resolve por definição:

func SplitBill(totalCents int, people int) (int, int) {
	return totalCents / people, totalCents % people
}

A garantia matemática de / e % sobre inteiros positivos é exatamente o invariante pedido: (totalCents/people)*people + totalCents%people == totalCents, com resto menor que o divisor. Nenhum centavo é criado nem destruído. Custo O(1). O caso 7, 10 — total menor que o grupo — não precisa de tratamento especial: 7/10 é 0 e 7%10 é 7 pela mesma regra.

A alternativa com float — total := float64(totalCents) / float64(people) e depois algum arredondamento — é a armadilha clássica de sistemas de pagamento. 1000/3 em float dá 333.333...; arredondando para baixo, 333*3 = 999 e um centavo evaporou sem registro; para cima, 334*3 = 1002 e dois centavos surgiram do nada. Auditoria de verdade reprova os dois. A lição transfere: quando o domínio é discreto — centavos, vagas, itens de estoque — a aritmética deve ser inteira do início ao fim, e a “sobra” deve ser um valor explícito que alguém decide o que fazer, não um erro de arredondamento escondido.

Exercício 3

Formate a etiqueta de preço

Junte as peças da aula: inteiros, divisão e resto, string e booleano. Implemente PriceLabel(name string, cents int, onSale bool) string que monta a etiqueta de exibição de um produto.

Entrada: o nome do produto, o preço em centavos (não negativo) e se está em promoção. Saída: a string "<name>: R$ <reais>.<centavos>", com os centavos sempre em dois dígitos; quando onSale é true, o prefixo "PROMO " antecede tudo.

Exemplos:

  • PriceLabel("Camiseta", 4990, false)"Camiseta: R$ 49.90";
  • PriceLabel("Caneca", 1950, true)"PROMO Caneca: R$ 19.50";
  • PriceLabel("Adesivo", 305, false)"Adesivo: R$ 3.05" — repare no zero: 3.5 estaria errado.

O caso 305 é o que separa soluções corretas de quase corretas: 305 % 1005, e a etiqueta precisa exibir 05. Procure na dica o verbo de formatação que resolve isso.

Solução & Explicação

Decomposição inteira para os valores, %02d para o zero à esquerda, if para o prefixo:

func PriceLabel(name string, cents int, onSale bool) string {
	label := fmt.Sprintf("%s: R$ %d.%02d", name, cents/100, cents%100)
	if onSale {
		return "PROMO " + label
	}
	return label
}

O verbo %02d significa “inteiro com largura mínima 2, preenchido com zeros”: 5 vira 05, 90 continua 90. Assim a etiqueta de 305 centavos sai R$ 3.05 sem nenhum caso especial. Sobre o booleano: if onSale já é a condição — escrever if onSale == true é redundância que nenhum código Go idiomático carrega. Custo O(n) no tamanho da etiqueta.

A alternativa tentadora é formatar um float: fmt.Sprintf("%s: R$ %.2f", name, float64(cents)/100). Para estes quatro casos de teste ela até passa — e é isso que a torna perigosa: o arredondamento de %.2f sobre uma fração binária inexata escolhe o centavo errado em algum valor que seu teste não cobriu, e o bug aparece em produção com cara de dado corrompido. A versão inteira não tem valor possível que a quebre, porque nenhuma conta sai do domínio dos inteiros. Quando as duas soluções têm o mesmo tamanho e uma delas é imune por construção, a escolha não é questão de estilo.

Resumo

  • Todo valor em Go tem um tipo fixo conhecido na compilação; os quatro do dia a dia são int, float64, string (imutável) e bool — e condições exigem bool de verdade, sem “truthy”.
  • Nada se converte sozinho: misturar tipos numéricos exige conversão explícita como float64(x), e a posição da conversão muda o resultado — converta o operando, não o resultado da conta.
  • Divisão entre inteiros descarta a fração e % entrega o resto; o par / e % decompõe valores discretos sem erro possível.
  • Floats seguem IEEE 754: 0.1 + 0.2 != 0.3 em Go como em toda linguagem; dinheiro se representa em centavos inteiros e só vira decimal na exibição, com %d.%02d.
  • %T no Printf revela o tipo de qualquer valor — sua ferramenta de investigação nas próximas aulas.

Na próxima aula, imersao/04-variaveis, você vê as formas de declarar variáveis — var, := e a inferência de tipos que dispensa escrever o que o compilador já sabe — e os zero values: o valor que toda variável de Go tem antes de você atribuir qualquer coisa.

Essa lição ficou clara?