← Painel 60 min
Primeiros passos

Constantes e iota: valores que o compilador garante que nunca mudam

Pré-requisitos: 04-variaveis

Intuição

Em uma cozinha profissional, existe o quadro de receitas e existe a bancada. Na bancada, tudo muda o tempo todo — panelas, temperos, quantidades em ajuste. No quadro, está o que não muda durante o serviço: a proporção da massa, a temperatura do forno, o tempo de cada assado. E há uma regra tácita: ninguém rabisca o quadro no meio do turno. Se a receita mudou, isso é uma decisão de outra hora, feita com calma, visível para todos. A distinção entre o que muda e o que não muda não é burocracia da cozinha: é o que permite a qualquer cozinheiro confiar no quadro sem conferir — e é exatamente essa confiança que esta aula traz para o código.

A aula passada foi sobre a bancada: variáveis, caixas de memória feitas para serem sobrescritas. Esta é sobre o quadro. Em Go, const declara um valor com nome que o compilador impede de mudar: const maxRetries = 3 seguido de qualquer tentativa de maxRetries = 4 nem compila. A diferença para uma variável “que você promete não alterar” é a mesma diferença entre o quadro e um post-it na bancada: a promessa é verificada por máquina, para todo o programa, para sempre.

Por que isso importa mais do que parece? Porque todo programa real está cheio de valores fixos — o limite de tentativas, o tamanho da página de resultados, o código do status “aprovado” — e a alternativa a constantes nomeadas são os números mágicos: literais soltos (if status == 2) cujo significado mora na cabeça de quem escreveu e evapora com o tempo. Imagine encontrar essa comparação em um código de seis meses atrás: dois o quê? Aprovado? Enviado? A segunda página? Você vai gastar dez minutos caçando a resposta em outros arquivos — e, pior, quando o valor precisar mudar, alguém vai fazer busca-e-troca de um literal que aparece em vinte lugares com vinte significados diferentes. const StatusApproved = 2 resolve a legibilidade; o fato de o compilador garantir a imutabilidade resolve a confiança — quando você lê maxRetries em qualquer ponto do programa, sabe o valor dele sem rastrear atribuições, porque não pode haver nenhuma.

Constantes em Go têm duas particularidades que as tornam mais interessantes que o final/const de outras linguagens — e são o recheio desta aula. A primeira: constantes numéricas podem ser não tipadas e ter precisão arbitrária no compilador, o que flexibiliza a regra rígida de tipos da aula 3 de um jeito controlado — const scale = 2 funciona tanto com int quanto com float64, porque o literal só ganha tipo definitivo no ponto de uso. Isso resolve um atrito real: sem essa regra, cada constante numérica precisaria existir em duas versões, uma para cada família de tipos, ou encher o código de conversões que não dizem nada. A segunda: o gerador iota, que produz sequências de constantes numeradas sem que você digite (nem erre) os números. Enumerações — conjuntos fechados de estados como “pendente, aprovado, enviado, entregue” — são onipresentes em qualquer sistema, e Go não tem uma palavra-chave enum: o idioma da linguagem é um bloco de constantes com iota, e você o verá em toda base de código Go do mercado.

Há ainda um benefício menos visível, mas que vale registrar: constantes são resolvidas durante a compilação, então não ocupam memória de variável nem custam leitura em tempo de execução — o compilador substitui cada uso pelo valor final, como se você tivesse digitado o literal ali, mas com nome, documentação e um único ponto de mudança. Isso também explica a limitação que a seção anterior mencionou: só pode ser constante aquilo que o compilador consegue calcular sozinho, sem rodar o programa. O resultado de uma chamada de função, por mais estável que pareça, nunca será uma constante em Go — e quando você tentar, a mensagem de erro dirá exatamente isso.

A pergunta prática que fecha a intuição: quando const, quando var? A resposta curta — que os exercícios vão exercitar — é: se o valor é conhecido ao escrever o código e não deve mudar durante a execução, é const. Se nasce de entrada, cálculo em runtime ou precisa ser reatribuído, é var/:=. O teste mental é simples: você conseguiria digitar o valor no código-fonte agora? O limite de tentativas, sim — const. O nome do usuário logado, impossível — só existe em runtime, é variável. Na dúvida, comece com const: o compilador reclama se você precisar mudar, e esse aviso é informação de design de graça — descobrir que um valor “fixo” precisa mudar em runtime é descobrir algo sobre o seu domínio.

Código anotado

O programa desta aula constrói três blocos de constantes de complexidade crescente — valores avulsos, uma enumeração com iota e a tabela de unidades binárias que é o exemplo canônico da linguagem — e demonstra a flexibilidade das constantes não tipadas.

Constantes avulsas: mesma sintaxe do var, palavra diferente, garantia diferente. Repare que maxRetries não declara tipo — isso a torna uma constante não tipada, ponto que o chunk 4 explora. Tentar maxRetries = 4 em qualquer lugar deste programa produziria cannot assign to maxRetries. Estas duas estão no nível do pacote, fora de qualquer função, e é onde constantes costumam morar: visíveis para o arquivo inteiro, perto do topo, onde quem abre o código as encontra primeiro.

package main

import "fmt"

const maxRetries = 3
const apiVersion = "v2"

Agora a enumeração. Dentro de um bloco const, iota vale 0 na primeira linha e cresce 1 a cada linha seguinte. E a linha que omite a expressão repete a expressão da linha anterior — por isso StatusApproved e StatusShipped ganham iota implicitamente, já incrementado. O ganho prático sobre digitar zero, um, dois e três à mão aparece na manutenção: inserir um status novo no meio da lista renumera tudo automaticamente, sem risco de dois status dividirem o mesmo número por erro de digitação. Os comentários mostram a expansão; confira um a um.

// Status de um pedido na loja.
const (
	StatusPending   = iota // 0
	StatusApproved         // 1
	StatusShipped          // 2
	StatusDelivered        // 3
)

iota participa de expressões, e é aí que ele deixa de ser um contador e vira um gerador de padrões. Aqui, o deslocamento de bits 1 << (10 * iota) produz as potências de 1024: KB, MB, GB. O _ na primeira linha é o identificador vazio — descarta o valor 1 que 1 << 0 produziria, porque “1 byte” não é uma unidade da tabela.

// Unidades binárias: cada uma é 1024 vezes a anterior.
const (
	_  = iota             // descarta iota=0
	KB = 1 << (10 * iota) // 1 << 10 = 1024
	MB                    // 1 << 20 = 1048576
	GB                    // 1 << 30 = 1073741824
)

E a demonstração da flexibilidade não tipada. scale não tem tipo declarado; nas duas linhas seguintes, o mesmo scale multiplica um int e um float64 — as duas compilam. Com uma variável scale := 2, a segunda linha seria o erro de mistura de tipos da aula 3. A constante não tipada adota o tipo que o contexto pede, no ponto de uso, sem abrir mão da checagem: const name = "Go" continuaria recusando name * count.

const scale = 2

func main() {
	count := 10
	weight := 0.25
	fmt.Println("count escalado:", count*scale)
	fmt.Println("weight escalado:", weight*scale)
	fmt.Println("pedido:", StatusShipped, "| 3 MB =", 3*MB, "bytes")
	fmt.Printf("retries=%d api=%s\n", maxRetries, apiVersion)
}

Uma regra para guardar do chunk 4: a flexibilidade é da constante, não do valor em runtime. Assim que count*scale é avaliado, scale ali é um int comum. A precisão arbitrária também é só em compilação: uma constante enorme (const big = 1 << 62) existe feliz no compilador, mas só pode ser usada onde couber no tipo de destino. Dito de outro jeito: as regras da aula 3 não foram revogadas — elas valem para todo valor que existe enquanto o programa roda. As constantes não tipadas vivem numa etapa anterior, dentro do compilador, onde ainda não há representação de memória para restringir; no instante em que tocam o mundo das variáveis, adotam um tipo e entram na disciplina comum. É uma flexibilidade com fronteira precisa, não uma porta dos fundos no sistema de tipos.

Experimente

Antes de rodar: qual o valor de Wednesday? E o que imprime a linha do float64? Confira suas previsões na saída.

package main

import "fmt"

const (
	Sunday = iota + 1 // começa em 1, não em 0
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

const factor = 3

func main() {
	fmt.Println("quarta-feira é o dia", Wednesday)
	fmt.Println("sábado é o dia", Saturday)
	fmt.Println("int:", 10*factor, "float64:", 2.5*factor)
}

Agora experimente quebrar: adicione Saturday = 99 dentro do main e leia o erro. Depois troque iota + 1 por iota * 2 e preveja os sete valores antes de rodar — a regra é sempre a mesma: a expressão se repete para cada linha com o contador incrementado, seja ela qual for. Por fim, o teste da constante tipada: mude const factor = 3 para const factor int = 3 e rode de novo — a linha do float64 para de compilar, porque um factor tipado como int volta a obedecer à regra estrita da aula 3. A diferença entre declarar e não declarar o tipo de uma constante é exatamente essa linha. Se sobrar curiosidade, um último teste: escreva const huge = 1 << 62 e imprima huge / GB — funciona, porque a conta inteira acontece no compilador e o resultado cabe em int.

Exercício 1

Dê nome aos níveis de log

O stub já traz a enumeração de níveis de log gerada com iotaLevelDebug a LevelError. Implemente LevelName(level int) string que devolve o nome do nível em maiúsculas: "DEBUG", "INFO", "WARNING", "ERROR". Para qualquer valor fora da tabela, devolva "UNKNOWN".

Entrada: um inteiro representando o nível. Saída: o nome correspondente.

Exemplos:

  • LevelName(LevelWarning)"WARNING";
  • LevelName(LevelDebug)"DEBUG";
  • LevelName(99)"UNKNOWN".

Regra do exercício (e da vida): dentro da função, compare com as constantes nomeadas, não com os números que você sabe que elas valem. O motivo está na solução.

Solução & Explicação

O formato do problema é uma tabela: para cada valor de entrada conhecido, uma saída fixa; para todo o resto, um valor padrão. A implementação mais direta espelha a tabela — uma comparação por nível, cada uma com return direto, e o caso desconhecido como última linha:

func LevelName(level int) string {
	if level == LevelDebug {
		return "DEBUG"
	}
	if level == LevelInfo {
		return "INFO"
	}
	if level == LevelWarning {
		return "WARNING"
	}
	if level == LevelError {
		return "ERROR"
	}
	return "UNKNOWN"
}

Custo O(1) — quatro comparações no pior caso. O return "UNKNOWN" final não precisa de else: se a execução chegou ali, nenhum nível bateu. Note também o que a função devolve para entrada inválida: um valor definido pelo contrato, não um panic nem uma string vazia — quem chama consegue tratar o caso sem surpresa. (Na aula 8 você reescreve exatamente esta função com switch, a ferramenta desenhada para este formato; a versão com ifs é a base honesta para comparar.)

A alternativa que passa nos testes hoje e falha no futuro: if level == 0 { return "DEBUG" }. Os números estão certos — até alguém inserir LevelTrace no topo do bloco const, o que desloca todos os iota uma posição. E esse tipo de inserção acontece: níveis de log, status de pedido e categorias de produto são listas vivas, que crescem conforme o sistema cresce. O autor da mudança vai olhar o bloco de constantes, ver que tudo continua compilando e seguir em frente — ele não tem como saber que, em outro arquivo, alguém comparou com o número cru em vez do nome. As comparações por nome continuam corretas automaticamente após essa mudança; as por número passam a mentir, e o pior tipo de mentira: sem erro de compilação, só logs classificados errado em produção. A constante nomeada não é decoração — é o único vínculo que sobrevive à evolução do código. Esse é, aliás, o argumento completo contra números mágicos: o literal 2 espalhado pelo código não tem como ser atualizado com segurança, porque nem todo 2 é LevelWarning.

Exercício 2

Formate bytes em unidades humanas

O stub traz a tabela KB/MB/GB do código anotado. Implemente HumanBytes(bytes int) string que formata um total de bytes na maior unidade que couber, com divisão inteira (arredonda para baixo). Abaixo de 1 KB, use "B".

Entrada: um total de bytes não negativo. Saída: "<valor> <unidade>".

Exemplos:

  • HumanBytes(512)"512 B";
  • HumanBytes(2048)"2 KB";
  • HumanBytes(5242880)"5 MB";
  • HumanBytes(2047)"1 KB" — arredonda para baixo;
  • HumanBytes(1023)"1023 B" — um byte a menos e a unidade muda.

Os casos 6 e 7 cercam a fronteira de 1024 pelos dois lados. A ordem em que você testa as unidades decide se a função funciona — pense antes de digitar.

Solução & Explicação

O enunciado “a maior unidade que couber” já dita a estrutura: as comparações precisam começar pela maior unidade, porque é ela que tem prioridade quando mais de uma serve. Da maior para a menor:

func HumanBytes(bytes int) string {
	if bytes >= GB {
		return fmt.Sprintf("%d GB", bytes/GB)
	}
	if bytes >= MB {
		return fmt.Sprintf("%d MB", bytes/MB)
	}
	if bytes >= KB {
		return fmt.Sprintf("%d KB", bytes/KB)
	}
	return fmt.Sprintf("%d B", bytes)
}

A ordem decrescente é o algoritmo: “a maior unidade que couber” significa que GB tem prioridade sobre MB, que tem prioridade sobre KB. Cada return corta a execução, então quem passa do primeiro if já se sabe menor que 1 GB, e assim por diante — as condições seguintes não precisam reafirmar esse limite superior, porque a estrutura já o garante. A divisão inteira da aula 3 faz o arredondamento para baixo de graça: 2047/1024 é 1, sem nenhuma chamada de arredondamento explícita. Custo O(1).

A armadilha é inverter a ordem: testar bytes >= KB primeiro captura todo valor acima de 1024 — incluindo os 5242880 bytes do caso 4, que sairiam como "5120 KB" em vez de "5 MB". A condição não está errada; está na posição errada, e é por isso que esse bug engana: cada if isolado é verdadeiro quando dispara, então nenhuma leitura linha a linha encontra o defeito — só a leitura da estrutura como um todo, perguntando qual condição captura o quê antes de quem. Em cadeias de faixas, a regra geral é testar da condição mais restritiva para a mais ampla. E repare no papel das constantes: bytes >= 1073741824 seria ilegível e conferível só com calculadora; bytes >= GB é autoexplicativo e gerado pelo compilador a partir do 1 << (10 * iota) — impossível digitar errado.

Exercício 3

Calcule a espera do pedido por dia da semana

O stub traz os dias da semana como enumeração — começando em 1, via iota + 1, e esse detalhe importa. Implemente ShippingDays(day int) int: pedidos feitos de segunda a sexta começam a ser processados no mesmo dia (devolva 0); no sábado, esperam 2 dias; no domingo, 1. Para qualquer valor fora de 1..7, devolva -1.

Entrada: um inteiro representando o dia. Saída: os dias de espera, ou -1 para entrada inválida.

Exemplos:

  • ShippingDays(Friday)0;
  • ShippingDays(Saturday)2;
  • ShippingDays(Sunday)1;
  • ShippingDays(0)-1 — repare: 0 é exatamente o zero value de int.

Este exercício combina a enumeração desta aula com os zero values da aula 4 — o caso 5 testa o que acontece quando alguém esquece de preencher o dia.

Solução & Explicação

A função tem quatro respostas possíveis, mas só três decisões reais: a entrada é válida? É um dos dois dias especiais? Tudo o mais é dia útil. A implementação espelha essa análise — valide primeiro, trate os especiais, deixe o comum para o fim:

func ShippingDays(day int) int {
	if day < Sunday || day > Saturday {
		return -1
	}
	if day == Saturday {
		return 2
	}
	if day == Sunday {
		return 1
	}
	return 0
}

A estrutura em retornos antecipados — inválido, especial, especial, comum — é a mesma do exercício 2 da aula 2, e vai ser o esqueleto da maior parte das suas funções Go: cada if elimina uma categoria, e a última linha atende quem sobrou (segunda a sexta), sem precisar reenumerá-los. Validar a entrada antes de qualquer outra decisão não é só organização: garante que as comparações seguintes trabalham sobre um valor que já provou estar no domínio, então nenhuma delas precisa se defender de novo. Os limites do intervalo usam Sunday e Saturday pelos nomes, pela mesma razão do exercício 1 — se a semana um dia mudar de numeração, a validação acompanha sem edição. Custo O(1).

Agora, o detalhe de design que o caso 5 cobra: a enumeração começa em 1 de propósito. Pense no caminho completo do bug evitado: um struct de pedido é criado em outra parte do sistema, alguém esquece de preencher o campo do dia, e o campo — como toda variável de Go — nasce com o zero value. Se Sunday fosse 0 — como sairia de um iota puro —, esse pedido esquecido seria silenciosamente um pedido de domingo, e a função responderia 1 com toda a confiança de quem recebeu um dado válido; nenhum teste de tipo, nenhum compilador e nenhum log apontaria o problema. Com a enumeração começando em 1, o zero value cai na faixa inválida e a função responde -1 — o esquecimento vira sinal detectável em vez de bug silencioso. Guarde esse truque: ao desenhar enumerações com iota, reserve o zero para “não definido” sempre que o zero value puder chegar à sua função por omissão. A armadilha correspondente é começar em iota puro por inércia e nunca perceber que o estado “vazio” coincide com o primeiro estado válido.

Resumo

  • const cria valores imutáveis verificados pelo compilador, em tempo de compilação; só números, strings e booleanos — e reatribuir não compila. Todo literal com significado de domínio merece virar constante nomeada.
  • Constantes não tipadas (const scale = 2) adotam o tipo do contexto de uso, flexibilizando a regra de mistura de tipos da aula 3 sem perder a checagem; declarar o tipo (const scale int = 2) devolve a rigidez.
  • iota vale 0 na primeira linha do bloco const e incrementa por linha; linhas sem expressão repetem a anterior, e expressões como iota + 1 ou 1 << (10 * iota) geram padrões.
  • Comparar com constantes nomeadas (nunca com seus valores literais) é o que mantém o código correto quando a enumeração evolui.
  • Comece enumerações em iota + 1 quando o zero value puder ser confundido com o primeiro estado válido — reserve o zero para “não definido”, como a biblioteca padrão faz nos meses de time.

Com isso o módulo Primeiros passos está completo. Na próxima aula, imersao/06-for, começa o controle de fluxo — e Go tem uma surpresa: um único loop, o for, que assume todas as formas de repetição que outras linguagens espalham por três ou quatro palavras-chave.

Essa lição ficou clara?