Estruturas de controle: if, for e switch

Estruturas de controle: if, for e switch

Controle de fluxo em Go: if com inicialização, as formas do for, incluindo o range sobre inteiros do Go 1.22, switch sem queda automática e com fallthrough explícito, rótulos, o break que só sai do switch e o defer, com seus argumentos avaliados na hora e o os.Exit que o ignora.
Golang

• • 14 min de leitura

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

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.

Comentários

Mais em Golang

Documentação com godoc e boas práticas de comentário
Documentação com godoc e boas práticas de comentário

Documentação em Go: comentários que começam pelo nome, cabeçalhos, listas e…

Clean Architecture em Go: camadas e dependências
Clean Architecture em Go: camadas e dependências

Clean Architecture em Go com código que compila: domínio sem dependências…

Armadilhas da concorrência: race conditions e como detectá-las
Armadilhas da concorrência: race conditions e como detectá-las

Armadilhas da concorrência em Go: corridas de dados e o detector -race…