Todo programa útil precisa tomar decisões e repetir operações. As estruturas de controle são os mecanismos que permitem isso. Go possui um conjunto deliberadamente pequeno: if, for e switch. Não existe while, não existe do while, não existe foreach como palavra-chave separada. Tudo é feito com variações do for. Essa simplicidade é intencional e, na prática, suficiente para qualquer situação.
O if em Go
A estrutura básica do if dispensa parênteses em torno da condição — diferente de C, Java e JavaScript — mas exige chaves {} mesmo quando o bloco tem apenas uma linha:
package main
import "fmt"
func main() {
temperatura := 38.5
if temperatura > 37.5 {
fmt.Println("Febre detectada.")
} else if temperatura >= 36.0 {
fmt.Println("Temperatura normal.")
} else {
fmt.Println("Hipotermia possível.")
}
}
Essa obrigatoriedade das chaves elimina uma categoria inteira de bugs famosos em outras linguagens, onde a indentação enganosa sugeria que uma linha fazia parte de um if quando na verdade não fazia.
Inicialização no if
Uma das características mais úteis e idiomáticas do Go é a possibilidade de executar uma instrução de inicialização dentro do próprio if, separada da condição por ponto e vírgula. A variável declarada nessa inicialização existe apenas dentro do escopo do bloco if/else:
package main
import (
"fmt"
"strconv"
)
func main() {
entrada := "42"
if valor, err := strconv.Atoi(entrada); err == nil {
fmt.Println("Conversão bem-sucedida:", valor*2)
} else {
fmt.Println("Erro na conversão:", err)
}
// "valor" e "err" não existem aqui fora
}
Esse padrão é extremamente comum em Go, especialmente ao trabalhar com funções que retornam um resultado e um erro. Ele mantém o escopo das variáveis de erro restrito ao bloco onde são relevantes, evitando poluição do escopo externo.
O for: único laço, múltiplas formas
Go tem apenas a palavra-chave for para todos os tipos de repetição. Ela assume formas diferentes dependendo de como é usada.
Forma clássica com três componentes — equivalente ao for do C:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
Forma com apenas a condição — equivalente ao while de outras linguagens:
n := 1
for n < 100 {
n *= 2
}
fmt.Println(n) // 128
Laço infinito — sem condição alguma, equivalente ao while(true):
contador := 0
for {
contador++
if contador >= 3 {
break
}
}
fmt.Println(contador) // 3
for range — para iterar sobre slices, arrays, maps, strings e channels e, desde o Go 1.22, sobre inteiros: for i := range 3 percorre 0, 1 e 2. Como toda novidade de linguagem, essa depende da diretiva do go.mod; com go 1.21 ali, o compilador recusa com requires go1.22 or later.
linguagens := []string{"Go", "Python", "Rust", "Java"}
for i, lang := range linguagens {
fmt.Printf("%d: %s\n", i, lang)
}
Quando o índice não é necessário, usa-se _:
for _, lang := range linguagens {
fmt.Println(lang)
}
Iterando sobre uma string com range, Go itera sobre os runes, não os bytes — o que significa que caracteres multibyte são tratados corretamente:
for i, r := range "Olá" {
fmt.Printf("índice %d: %c\n", i, r)
}
// índice 0: O
// índice 1: l
// índice 2: á ← ocupa 2 bytes, mas é um único rune
Iterando sobre um map:
capitais := map[string]string{
"Brasil": "Brasília",
"Argentina": "Buenos Aires",
"Chile": "Santiago",
}
for pais, capital := range capitais {
fmt.Printf("%s → %s\n", pais, capital)
}
A ordem de iteração sobre maps em Go é intencionalmente aleatória. O runtime até sorteia o ponto de partida a cada laço, justamente para que ninguém passe a depender de uma ordem que não é garantida. Quando a ordem importa, como num relatório, colete as chaves numa fatia, ordene com slices.Sorted(maps.Keys(capitais)) e percorra a fatia.
break e continue
O break interrompe o laço imediatamente. O continue pula para a próxima iteração:
for i := 0; i < 10; i++ {
if i == 3 {
continue // pula o 3
}
if i == 7 {
break // para no 7
}
fmt.Println(i)
}
// Imprime: 0 1 2 4 5 6
Go também suporta labels para break e continue em laços aninhados, permitindo sair de um laço externo a partir de um laço interno:
externo:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break externo // sai do laço externo
}
fmt.Printf("i=%d j=%d\n", i, j)
}
}
O switch em Go
O switch do Go é mais poderoso e mais seguro do que o de C ou Java. A diferença mais importante: não há fall-through automático. Cada case executa apenas seu próprio bloco e para — não é necessário escrever break ao final de cada caso.
package main
import "fmt"
func main() {
dia := 3
switch dia {
case 1:
fmt.Println("Segunda-feira")
case 2:
fmt.Println("Terça-feira")
case 3:
fmt.Println("Quarta-feira")
case 4:
fmt.Println("Quinta-feira")
case 5:
fmt.Println("Sexta-feira")
case 6, 7:
fmt.Println("Final de semana")
default:
fmt.Println("Dia inválido")
}
}
Note que um case pode listar múltiplos valores separados por vírgula, como case 6, 7.
Inicialização no switch
Assim como o if, o switch aceita uma instrução de inicialização:
switch os := runtime.GOOS; os {
case "linux":
fmt.Println("Sistema Linux")
case "darwin":
fmt.Println("Sistema macOS")
default:
fmt.Printf("Outro sistema: %s\n", os)
}
switch sem expressão
Quando o switch não recebe uma expressão, ele se comporta como uma cadeia de if/else if, avaliando cada case como uma expressão booleana independente. Esse padrão é muito mais legível do que um longo encadeamento de if:
nota := 78
switch {
case nota >= 90:
fmt.Println("Conceito A")
case nota >= 80:
fmt.Println("Conceito B")
case nota >= 70:
fmt.Println("Conceito C")
case nota >= 60:
fmt.Println("Conceito D")
default:
fmt.Println("Reprovado")
}
fallthrough explícito
Se o comportamento de queda entre casos for necessário, Go exige que seja declarado explicitamente com a palavra-chave fallthrough. Ela força a execução do próximo case independentemente de sua condição:
x := 1
switch x {
case 1:
fmt.Println("caso 1")
fallthrough
case 2:
fmt.Println("caso 2 — executado por fallthrough")
case 3:
fmt.Println("caso 3 — não executado")
}
// Imprime:
// caso 1
// caso 2 — executado por fallthrough
O fallthrough é raro no código Go idiomático. Seu uso deve ser acompanhado de um comentário explicando a intenção.
defer: execução adiada
Embora não seja estritamente uma estrutura de controle de fluxo, o defer merece introdução aqui por influenciar diretamente a ordem de execução. Uma instrução defer agenda a execução de uma função para o momento em que a função atual retornar — independentemente de como ela retornar, seja por um retorno normal ou por um pânico:
package main
import "fmt"
func main() {
fmt.Println("início")
defer fmt.Println("executado por defer")
fmt.Println("fim")
}
// Saída:
// início
// fim
// executado por defer
Quando múltiplos defer são empilhados, eles executam em ordem LIFO — o último declarado é o primeiro a executar:
for i := 1; i <= 3; i++ {
defer fmt.Println(i)
}
// Imprime: 3, 2, 1
O caso de uso mais comum do defer é garantir o fechamento de recursos como arquivos, conexões de banco de dados ou locks de mutex, logo após sua abertura:
arquivo, err := os.Open("dados.txt")
if err != nil {
log.Fatal(err)
}
defer arquivo.Close() // será executado quando a função retornar
// ... trabalha com o arquivo
Esse padrão garante que o recurso seja sempre liberado, mesmo que a função retorne por um caminho de erro inesperado.
Três palavras-chave dão conta de todo o controle de fluxo do Go: if e switch, ambos com inicialização embutida, e um for que faz o papel de todos os laços. O defer completa o quadro, com duas regras que valem a pena guardar desde já — os argumentos são avaliados na hora em que ele é registrado, e o os.Exit passa por cima dele. E o break dentro de um switch sai só do switch: quando o alvo é o laço, é o rótulo que resolve.
Fontes e leituras recomendadas
-
Especificação da linguagem Go — Statements — Referência formal de todas as estruturas de controle. https://go.dev/ref/spec#Statements
-
A Tour of Go — Flow control — Seção interativa sobre if, for e switch. https://go.dev/tour/flowcontrol/1
-
Go by Example: For — Exemplos práticos das formas do laço for. https://gobyexample.com/for
-
Go by Example: Switch — Exemplos comentados do switch em Go. https://gobyexample.com/switch
-
Go by Example: Defer — Exemplos e casos de uso do defer. https://gobyexample.com/defer
-
Effective Go — Control structures — Boas práticas oficiais para uso de estruturas de controle. https://go.dev/doc/effective_go#control-structures
Exercícios
Exercício 1
Um laço lê comandos de uma fila e usa um switch para tratá-los. No case "sair":, o programador escreve break, esperando encerrar o laço. O programa continua lendo comandos para sempre. Por quê, e quais são as duas formas idiomáticas de corrigir?
Ver resposta
✓ Resposta: Dentro de um switch (ou de um select), break encerra o switch, que é a instrução mais interna que ele alcança, e não o for em volta. Num teste com for i := 0; i < 5; i++ e um switch cujo case 2: só faz break, o programa imprime 0 1 2 3 4, sem interromper nada. Quem vem do C estranha duplamente, porque lá o break no switch é obrigatório para evitar a queda entre casos, e em Go ele nem é necessário para isso. A primeira correção é o rótulo que o artigo apresenta: marcar o laço com fila: e escrever break fila no caso. A segunda, muitas vezes mais limpa, é tirar o laço para uma função própria e usar return no caso de saída, o que também deixa o laço testável. Um continue dentro do switch, por outro lado, age sobre o for, porque switch não é laço; a assimetria entre os dois é justamente o que confunde.
Exercício 2
Uma função faz x := 1, depois defer fmt.Println("x =", x) e defer func() { fmt.Println("x na closure =", x) }(), e em seguida x = 2. O que cada defer imprime, e em que ordem?
Ver resposta
✓ Resposta: A saída é x na closure = 2 e depois x = 1. A ordem vem da pilha: o último defer registrado é o primeiro a rodar, como o artigo mostra. Os valores vêm de uma regra que o artigo não menciona: os argumentos de uma chamada adiada são avaliados no momento em que o defer é executado, e não quando a função retorna. No primeiro caso, x é argumento do Println, então o valor 1 foi copiado na hora e a mudança posterior não o afeta. No segundo, a função anônima não recebe argumento nenhum: ela enxerga a variável x em si e a lê só no fim, quando já vale 2. A regra tem consequência prática em código como defer log.Printf("duração: %v", time.Since(inicio)), que mede quase zero, porque o time.Since roda na linha do defer. A forma certa é defer func() { log.Printf("duração: %v", time.Since(inicio)) }().
Exercício 3
O exemplo do artigo abre um arquivo, trata o erro com log.Fatal(err) e registra defer arquivo.Close(). Um colega copia o padrão e, mais adiante na mesma função, usa log.Fatal também para um erro de validação, contando que o defer feche o arquivo e grave um log de auditoria registrado antes. O arquivo é fechado? O log de auditoria sai?
Ver resposta
✓ Resposta: Nenhum dos dois. O log.Fatal imprime a mensagem e chama os.Exit(1), que encerra o processo na hora, sem desempilhar funções e, portanto, sem rodar nenhum defer. Num teste com defer fmt.Println("defer antes do Fatal") seguido de log.Fatal("saindo"), a saída traz só a linha do log e o código de saída 1; a mensagem do defer nunca aparece. A frase do artigo, de que o defer roda "independentemente de como a função retornar", vale para return e para pânico, mas os.Exit não é um retorno. O arquivo aberto só para leitura é liberado pelo sistema operacional quando o processo morre, então ali o prejuízo é pequeno; já buffers não descarregados, arquivos temporários e o log de auditoria se perdem. A prática é usar log.Fatal e os.Exit só no main, de preferência numa linha só, e fazer as outras funções devolverem error para quem decide.
Exercício 4
Um script processa 50 mil arquivos num laço: para cada um, os.Open, defer f.Close() e a leitura. Depois de alguns milhares de iterações, o programa falha com too many open files. Se o defer garante o fechamento, onde está o vazamento?
Ver resposta
✓ Resposta: O defer garante o fechamento quando a função retorna, e não ao fim de cada volta do laço. Todos os f.Close() ficam empilhados à espera do retorno da função que contém o for, e até lá cada arquivo aberto continua aberto. Quando o número de descritores passa do limite do processo (o ulimit -n mostra esse limite; 1024 é um valor padrão frequente, mas ele varia muito — num WSL com configuração de fábrica, por exemplo, passa de um milhão, e o vazamento demora a aparecer), o os.Open seguinte falha. A correção idiomática é pôr o corpo da iteração numa função própria, como processar(caminho) error, com o os.Open e o defer f.Close() lá dentro; o fechamento passa a acontecer a cada chamada. Outra saída é chamar f.Close() explicitamente ao fim de cada volta, mas ela é frágil, porque qualquer continue ou return antecipado reabre o vazamento. O sinal de alerta vale para qualquer recurso: defer dentro de for quase sempre pede uma função extra.
Exercício 5
Um desenvolvedor gosta do padrão if valor, err := strconv.Atoi(entrada); err != nil { return err } e, na linha seguinte ao if, tenta usar valor. O código não compila. Explique o motivo e mostre as duas formas corretas de escrever, dizendo quando cada uma é preferível.
Ver resposta
✓ Resposta: As variáveis declaradas na inicialização do if existem só dentro dele, incluindo os ramos else if e else, e deixam de existir na chave de fechamento, como o artigo explica. Na linha seguinte, valor é um nome desconhecido, e o compilador acusa undefined: valor. A primeira forma correta é declarar antes, valor, err := strconv.Atoi(entrada), e só depois testar if err != nil { return err }. Ela é a preferível quando o valor será usado depois, que é o caso mais comum, e é a que se vê na maior parte do código Go. A segunda é manter a forma curta e usar o valor no else, if valor, err := ...; err != nil { return err } else { usar(valor) }, mas o revive, sucessor do antigo golint, aponta esse else depois de um return na regra indent-error-flow, e os revisores costumam pedir o mesmo. A forma curta brilha quando a variável realmente não interessa fora do teste, como em if err := f.Close(); err != nil { ... }, em que só o erro importa.