Go Admin

Estruturas de Controle em Go: if, for, switch e defer na Prática Já leu

Estruturas de Controle em Go: if, for, switch e defer Go é uma linguagem que prioriza simplicidade e clareza. Suas estruturas de controle refletem essa filosofia: são diretas, sem sintaxe desnecessária e com comportamentos bem definidos. Neste artigo, você aprenderá a dominar os quatro pilares do controle de fluxo em Go de forma prática e fundamentada. Cada estrutura tem seu propósito específico, e entendê-las profundamente é essencial para escrever código Go idiomático e eficiente. A abordagem aqui é progressiva: começamos com as decisões simples (if), passamos por iterações (for), depois ramificações múltiplas (switch) e finalizamos com um conceito único de Go (defer). Você não apenas aprenderá a sintaxe, mas compreenderá o "porquê" por trás de cada decisão de design. Condicional if: Decisões Simples e Compostas O if em Go é tão minimalista quanto parece. Não há parênteses obrigatórios ao redor da condição, mas as chaves são obrigatórias, mesmo que o bloco tenha uma única linha. Isso força uma consistência visual

Estruturas de Controle em Go: if, for, switch e defer

Go é uma linguagem que prioriza simplicidade e clareza. Suas estruturas de controle refletem essa filosofia: são diretas, sem sintaxe desnecessária e com comportamentos bem definidos. Neste artigo, você aprenderá a dominar os quatro pilares do controle de fluxo em Go de forma prática e fundamentada. Cada estrutura tem seu propósito específico, e entendê-las profundamente é essencial para escrever código Go idiomático e eficiente.

A abordagem aqui é progressiva: começamos com as decisões simples (if), passamos por iterações (for), depois ramificações múltiplas (switch) e finalizamos com um conceito único de Go (defer). Você não apenas aprenderá a sintaxe, mas compreenderá o "porquê" por trás de cada decisão de design.

Condicional if: Decisões Simples e Compostas

O if em Go é tão minimalista quanto parece. Não há parênteses obrigatórios ao redor da condição, mas as chaves são obrigatórias, mesmo que o bloco tenha uma única linha. Isso força uma consistência visual no código que Go cultiva deliberadamente.

A forma básica é direta: você avalia uma expressão booleana e executa um bloco caso seja verdadeira. Mas há nuances importantes que vão além do óbvio.

package main

import "fmt"

func main() {
    idade := 25

    // if simples
    if idade >= 18 {
        fmt.Println("Você é maior de idade")
    }

    // if com else
    if idade < 13 {
        fmt.Println("Criança")
    } else if idade < 18 {
        fmt.Println("Adolescente")
    } else {
        fmt.Println("Adulto")
    }
}

Um aspecto poderoso do if em Go é a possibilidade de declarar e avaliar uma variável na mesma linha. Isso é particularmente útil ao trabalhar com funções que retornam um valor e um erro. A variável declarada nesse contexto é escopo-limitada ao bloco if (e seus else), o que reduz a poluição de variáveis globais.

package main

import (
    "fmt"
    "strconv"
)

func main() {
    numero := "42"

    // Declarar e usar uma variável no if
    if valor, err := strconv.Atoi(numero); err == nil {
        fmt.Printf("Número convertido com sucesso: %d\n", valor)
    } else {
        fmt.Printf("Erro na conversão: %v\n", err)
    }

    // valor não existe aqui — escopo limitado ao if
}

Isso é mais do que conveniência sintática: é uma filosofia de Go sobre manter variáveis próximas ao seu ponto de uso e evitar estado global desnecessário.

Iteração for: O Único Loop de Go

Ao contrário de linguagens como Python ou Java, Go possui apenas uma palavra-chave para iteração: for. Não há while, do-while ou foreach separados. Tudo é for, mas com múltiplas formas que cobrem todos os casos de uso.

A primeira forma é a clássica, com inicialização, condição e incremento. A inicialização é opcional, a condição determina quando parar, e o incremento (ou qualquer comando) executa após cada iteração.

package main

import "fmt"

func main() {
    // for clássico
    for i := 0; i < 5; i++ {
        fmt.Printf("Iteração %d\n", i)
    }

    // for como while (apenas condição)
    contador := 0
    for contador < 3 {
        fmt.Printf("Contador: %d\n", contador)
        contador++
    }

    // for infinito
    // for {
    //     fmt.Println("Executa para sempre até break")
    //     break
    // }
}

A segunda forma crucial é o range, que itera sobre coleções. Com range, você obtém o índice e o valor (ou apenas um deles). Isso é extremamente útil e elimina classes inteiras de bugs relacionados a gerenciamento manual de índices.

package main

import "fmt"

func main() {
    nomes := []string{"Alice", "Bob", "Charlie"}

    // índice e valor
    for i, nome := range nomes {
        fmt.Printf("%d: %s\n", i, nome)
    }

    // apenas valor (descarta índice com _)
    for _, nome := range nomes {
        fmt.Printf("Nome: %s\n", nome)
    }

    // apenas índice
    for i := range nomes {
        fmt.Printf("Índice: %d\n", i)
    }

    // iterando sobre mapa
    mapa := map[string]int{"x": 10, "y": 20}
    for chave, valor := range mapa {
        fmt.Printf("%s: %d\n", chave, valor)
    }
}

Um detalhe crítico: ao iterar sobre um mapa com range, a ordem não é garantida. Isso é uma decisão deliberada de Go para evitar que código dependa de uma ordem que não está documentada. Se você precisa de ordem, deve ordenar explicitamente antes de iterar.

Switch: Ramificação Limpa para Múltiplas Condições

O switch em Go é elegante e poderoso. Diferentemente de muitas linguagens, Go não requer break entre cases — a execução automaticamente "sai" após um case correspondente, a menos que você use fallthrough explicitamente.

Isso elimina uma das fontes mais comuns de bugs em linguagens como C ou JavaScript, onde esquecer o break causa comportamento inesperado.

package main

import "fmt"

func main() {
    dia := 3

    // switch básico
    switch dia {
    case 1:
        fmt.Println("Segunda-feira")
    case 2:
        fmt.Println("Terça-feira")
    case 3:
        fmt.Println("Quarta-feira")
    default:
        fmt.Println("Dia inválido")
    }
}

Você pode ter múltiplos valores em um único case, separados por vírgulas. Isso reduz duplicação quando vários casos devem executar o mesmo código.

package main

import "fmt"

func main() {
    caractere := 'A'

    switch caractere {
    case 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U':
        fmt.Println("Vogal")
    default:
        fmt.Println("Consoante ou não-letra")
    }
}

Go também permite switch sem uma expressão inicial, onde cada case é uma condição booleana completa. Isso é essencialmente syntactic sugar para uma série de if-else if, mas é frequentemente mais legível.

package main

import "fmt"

func main() {
    idade := 25

    switch {
    case idade < 13:
        fmt.Println("Criança")
    case idade < 18:
        fmt.Println("Adolescente")
    case idade < 65:
        fmt.Println("Adulto")
    default:
        fmt.Println("Idoso")
    }
}

O fallthrough é explícito e raro. Quando você o usa, fica claro na leitura do código que a intenção é continuar para o próximo case — não é um acidente, é uma decisão documentada.

package main

import "fmt"

func main() {
    numero := 2

    switch numero {
    case 1:
        fmt.Println("Um")
        fallthrough
    case 2:
        fmt.Println("Um ou Dois")
        fallthrough
    case 3:
        fmt.Println("Um, Dois ou Três")
    default:
        fmt.Println("Outro")
    }
    // Saída: Um ou Dois / Um, Dois ou Três
}

Defer: Adiando Execução com Garantia

O defer é um recurso único de Go que não existe em muitas outras linguagens. Ele permite que você agende uma função para ser executada após a função atual retornar. Parece simples, mas suas aplicações são profundas.

A principal utilidade do defer é garantir que certos códigos de limpeza sejam sempre executados, mesmo que exceções ocorram (ou, em Go, mesmo que você retorne prematuramente). Isso é particularmente valioso ao trabalhar com arquivos, conexões de banco de dados ou locks.

package main

import (
    "fmt"
    "os"
)

func main() {
    arquivo, err := os.Create("teste.txt")
    if err != nil {
        fmt.Println("Erro ao criar arquivo:", err)
        return
    }

    // defer garante que Close será chamado
    defer arquivo.Close()

    arquivo.WriteString("Olá, Go!\n")

    fmt.Println("Arquivo escrito com sucesso")
    // arquivo.Close() é chamado automaticamente ao sair da função
}

Um comportamento crucial a entender: o defer adiam a execução, mas os argumentos são avaliados imediatamente. Se você passar o valor de uma variável, aquele valor é "congelado" no momento do defer.

package main

import "fmt"

func main() {
    contador := 1

    defer fmt.Println("Defer 1:", contador)  // valor 1 é "congelado"

    contador = 2

    defer fmt.Println("Defer 2:", contador)  // valor 2 é "congelado"

    contador = 3

    fmt.Println("Durante execução:", contador)

    // Saída:
    // Durante execução: 3
    // Defer 2: 2
    // Defer 1: 1
}

Note que defers são executados em ordem LIFO (Last In, First Out) — o último defer registrado é o primeiro a executar. Isso é intencional: funciona como uma pilha, permitindo que você estabeleça dependências de limpeza na ordem correta.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mutex sync.Mutex

    // Adquire lock
    mutex.Lock()
    defer mutex.Unlock()  // Garante que unlock sempre ocorrerá

    fmt.Println("Seção crítica protegida")

    // Mesmo que você retorne aqui, Unlock será chamado
    // Mesmo que panic ocorra, Unlock será chamado
}

Uma aplicação avançada é usar defer com funções anônimas que capturam variáveis por referência, permitindo lógica de limpeza mais sofisticada.

package main

import "fmt"

func main() {
    valor := "inicial"

    defer func() {
        fmt.Println("Limpeza final, valor é:", valor)
    }()

    valor = "modificado"

    // Saída: Limpeza final, valor é: modificado
}

Combinando Estruturas: Padrões Práticos

Na prática, você raramente usa essas estruturas isoladamente. Elas trabalham juntas para resolver problemas reais. Um padrão comum é combinar for com if para filtrar dados, ou usar switch dentro de um for para diferentes casos.

package main

import "fmt"

func main() {
    numeros := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    fmt.Println("Números pares maiores que 4:")
    for _, numero := range numeros {
        if numero > 4 && numero%2 == 0 {
            fmt.Println(numero)
        }
    }
}

Outro padrão essencial é usar defer para garantir limpeza dentro de loops ou funções complexas que fazem múltiplas alocações.

package main

import (
    "fmt"
    "os"
)

func processarArquivos(nomes []string) {
    for _, nome := range nomes {
        arquivo, err := os.Open(nome)
        if err != nil {
            fmt.Printf("Erro ao abrir %s: %v\n", nome, err)
            continue
        }

        defer arquivo.Close()  // Garante fechamento mesmo com continue

        // processa arquivo
        fmt.Printf("Processando %s\n", nome)
    }
}

func main() {
    processarArquivos([]string{"arquivo1.txt", "arquivo2.txt"})
}

Entender quando cada estrutura é apropriada é a marca de um programador Go competente. Use if para lógica simples, for para iterações, switch para múltiplas ramificações sobre um valor específico, e defer para garantir limpeza. Essa combinação cobre praticamente todos os cenários de controle de fluxo que você encontrará.

Referências


Artigos relacionados