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 iota — LevelDebug 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 deint.
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
constcria 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. iotavale 0 na primeira linha do blococonste incrementa por linha; linhas sem expressão repetem a anterior, e expressões comoiota + 1ou1 << (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 + 1quando 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 detime.
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.