Closures são funções que referenciam variáveis de seu escopo léxico, mesmo após o escopo externo ter retornado. Em Go, closures são amplamente utilizadas para criar funções de ordem superior, adiar execuções e capturar estado. Nesta aula, vamos mergulhar nos detalhes de como closures capturam variáveis, os perigos comuns em loops, casos de uso práticos e como elas afetam a memória do programa.

Entender closures é essencial para escrever código idiomático e eficiente em Go, especialmente ao trabalhar com goroutines, callbacks e pipelines de dados. Vamos explorar desde a sintaxe básica até questões avançadas de captura e gerenciamento de memória.

Captura de variáveis

Em Go, closures capturam variáveis por referência, não por valor. Isso significa que a closure mantém uma referência à variável original, e não uma cópia do valor no momento da criação. Isso é diferente de algumas linguagens que capturam por valor, como C# (com ressalvas) ou JavaScript (com let).

Quando você define uma closure dentro de uma função, ela tem acesso às variáveis locais dessa função. Essas variáveis são alocadas no heap, se necessário, para que a closure possa acessá-las mesmo após a função retornar. Vamos ver um exemplo simples:

package main

import "fmt"

func main() {
    x := 10
    increment := func() {
        x++
    }
    increment()
    increment()
    fmt.Println(x) // 12
}

No exemplo acima, a closure increment modifica a variável x que foi declarada no escopo da função main. A closure captura a variável por referência, então as alterações são refletidas na variável original.

É importante notar que cada chamada à closure opera sobre a mesma variável. Se você criar múltiplas closures no mesmo escopo, todas compartilham a mesma variável. Isso pode ser útil ou perigoso, dependendo do contexto.

Armadilhas em loops

Um dos erros mais comuns com closures em Go (e em muitas linguagens) é capturar variáveis de loop de forma incorreta. Antes de Go 1.22, a variável de iteração em um loop for era reutilizada a cada iteração, o que causava problemas clássicos quando closures eram criadas dentro do loop. Vamos ver o exemplo clássico:

package main

import "fmt"

func main() {
    var funcs []func()
    for i := 0; i < 3; i++ {
        funcs = append(funcs, func() {
            fmt.Println(i)
        })
    }
    for _, f := range funcs {
        f()
    }
}

Antes do Go 1.22, a saída seria 3 3 3, porque todas as closures capturavam a mesma variável i, que ao final do loop valia 3. A partir do Go 1.22, a variável de loop é criada nova a cada iteração, então a saída agora é 0 1 2. Se você estiver usando uma versão anterior, é necessário criar uma cópia da variável dentro do loop:

for i := 0; i < 3; i++ {
    i := i // cria uma nova variável local
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}

Essa armadilha também ocorre em loops range. Por exemplo, ao iterar sobre um slice e capturar o índice ou valor:

for _, v := range slice {
    v := v // copia necessária antes de Go 1.22
    funcs = append(funcs, func() { fmt.Println(v) })
}

Recomenda-se sempre testar o comportamento na versão do Go que você está usando. Para garantir compatibilidade e evitar surpresas, muitos desenvolvedores ainda adotam a prática de criar uma variável local temporária, mesmo em versões recentes, pois é uma boa prática de clareza.

Casos de uso

Closures são extremamente versáteis e têm muitos casos de uso em Go. Alguns dos mais comuns incluem:

  • Funções de ordem superior: Passar closures como argumentos para funções como map, filter e reduce (embora Go não tenha essas funções nativas para slices, você pode implementá-las).
  • Callbacks: Em bibliotecas de rede ou I/O, closures são usadas para tratar eventos ou respostas.
  • Adiamento de execução: Com defer, você pode usar closures para capturar valores no momento da chamada, não no momento da execução.
  • Geradores e fábricas de funções: Criar funções que retornam outras funções com estado interno.
  • Goroutines e concorrência: Muitas vezes, você precisa capturar variáveis específicas para cada goroutine.

Vamos ver um exemplo prático de uma fábrica de funções:

package main

import "fmt"

func adder(initial int) func(int) int {
    return func(delta int) int {
        initial += delta
        return initial
    }
}

func main() {
    add10 := adder(10)
    add100 := adder(100)
    fmt.Println(add10(5))  // 15
    fmt.Println(add100(5)) // 105
}

Neste exemplo, cada closure mantém seu próprio estado independente, pois cada chamada a adder cria uma nova variável initial.

Outro caso comum é usar closures para capturar valores em loops para goroutines:

for i := 0; i < 3; i++ {
    go func(i int) {
        fmt.Println(i)
    }(i)
}

Aqui, passamos i como argumento, criando uma cópia para cada goroutine, evitando a armadilha de captura.

Memória

Closures têm implicações de memória importantes. Quando uma closure captura variáveis, essas variáveis são movidas para o heap (escape analysis) para que a closure possa acessá-las mesmo após a função retornar. Isso significa que o uso excessivo de closures pode aumentar a pressão no garbage collector e o consumo de memória.

Vamos analisar um exemplo de escape:

func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

Neste caso, a variável count escapa para o heap, porque a closure a referencia. Se você criar muitas closures que capturam grandes estruturas de dados, a memória pode ser usada de forma ineficiente.

Algumas boas práticas para minimizar problemas de memória:

  • Use closures apenas quando necessário. Se você não precisa capturar estado, prefira funções regulares.
  • Evite capturar variáveis grandes desnecessariamente. Se possível, passe valores como argumentos.
  • Esteja ciente de que closures mantêm referências a objetos, impedindo que o GC os libere enquanto a closure existir.
  • Em loops, evite criar closures desnecessárias, especialmente em grandes iterações, pois cada closure pode alocar memória.

O compilador Go otimiza bem o escape analysis, mas é importante entender o que está acontecendo para escrever código eficiente.

Boas práticas

Além das considerações de memória, aqui estão algumas boas práticas ao usar closures:

  • Prefira passar variáveis como argumentos em vez de capturá-las, quando a intenção for usar o valor atual.
  • Use closures para encapsular estado, mas mantenha a lógica simples para legibilidade.
  • Documente closures complexas com comentários claros.
  • Em versões anteriores ao Go 1.22, sempre crie cópias de variáveis de loop para evitar bugs.
  • Teste closures em cenários de concorrência para garantir que não há corridas de dados.

Referências

Exercícios

  1. Exercício 1: Escreva uma função makeMultiplier(factor int) que retorna uma closure que multiplica seu argumento pelo fator. Teste com diferentes fatores.
  2. ✓ Resposta:
    func makeMultiplier(factor int) func(int) int {
        return func(x int) int {
            return x * factor
        }
    }
    
    func main() {
        double := makeMultiplier(2)
        triple := makeMultiplier(3)
        fmt.Println(double(5)) // 10
        fmt.Println(triple(5)) // 15
    }
  3. Exercício 2: Crie um loop que itera de 0 a 4 e cria uma slice de closures que imprima o valor esperado (0,1,2,3,4) ao serem chamadas. Use a técnica de cópia para garantir compatibilidade com versões antigas.
  4. ✓ Resposta:
    var funcs []func()
    for i := 0; i < 5; i++ {
        i := i
        funcs = append(funcs, func() { fmt.Println(i) })
    }
    for _, f := range funcs {
        f() // imprime 0 1 2 3 4
    }
  5. Exercício 3: Escreva uma função filter que receba um slice de inteiros e uma closure de predicado, e retorne um novo slice contendo apenas os elementos que satisfazem o predicado.
  6. ✓ Resposta:
    func filter(nums []int, pred func(int) bool) []int {
        var result []int
        for _, n := range nums {
            if pred(n) {
                result = append(result, n)
            }
        }
        return result
    }
    
    func main() {
        nums := []int{1, 2, 3, 4, 5}
        evens := filter(nums, func(n int) bool { return n%2 == 0 })
        fmt.Println(evens) // [2 4]
    }
  7. Exercício 4: Explique por que o seguinte código imprime "10 10 10" e como corrigi-lo para imprimir "0 1 2" (antes do Go 1.22):
  8. for i := 0; i < 3; i++ {
        defer func() { fmt.Println(i) }()
    }

    ✓ Resposta: O código imprime "10 10 10" porque as closures adiadas capturam a variável i por referência, e quando defer executa (após o loop), i já vale 10. Para corrigir, passe i como argumento: defer func(i int) { fmt.Println(i) }(i).
  9. Exercício 5: Crie uma closure que conte chamadas e use-a para implementar um gerador de números sequenciais (0,1,2,...).
  10. ✓ Resposta:
    func generator() func() int {
        count := -1
        return func() int {
            count++
            return count
        }
    }
    
    func main() {
        next := generator()
        fmt.Println(next()) // 0
        fmt.Println(next()) // 1
        fmt.Println(next()) // 2
    }