Closures em Go

1. O que são Closures?

Em Go, uma closure é uma função que captura e mantém referências a variáveis do escopo externo onde foi definida. Isso significa que a closure "lembra" do ambiente onde foi criada, mesmo após esse escopo externo ter terminado sua execução.

Uma closure pode ser entendida como uma função anônima com estado. Enquanto funções anônimas comuns executam uma operação isolada, closures carregam consigo um contexto que pode ser modificado e consultado ao longo do tempo.

package main

import "fmt"

func main() {
    mensagem := "Olá"

    // Closure que captura a variável 'mensagem'
    closure := func() {
        fmt.Println(mensagem) // Acessa variável do escopo externo
    }

    closure() // Output: Olá
}

A diferença fundamental entre uma função anônima simples e uma closure é que a closure mantém acesso às variáveis do escopo onde foi criada.

2. Sintaxe e Criação de Closures

Closures podem ser declaradas inline, atribuídas a variáveis ou retornadas de funções.

package main

import "fmt"

func main() {
    // Declaração inline
    func() {
        fmt.Println("Executando closure inline")
    }()

    // Atribuição a variável
    contador := 0
    incrementa := func() int {
        contador++
        return contador
    }

    fmt.Println(incrementa()) // 1
    fmt.Println(incrementa()) // 2

    // Retorno de função
    saudacao := criaSaudacao("Bom dia")
    fmt.Println(saudacao("João")) // Bom dia, João!
}

func criaSaudacao(saudacao string) func(string) string {
    return func(nome string) string {
        return fmt.Sprintf("%s, %s!", saudacao, nome)
    }
}

3. Captura de Variáveis por Referência

Closures em Go capturam variáveis por referência, não por valor. Isso significa que múltiplas closures compartilhando a mesma variável externa verão as alterações feitas por qualquer uma delas.

package main

import "fmt"

func main() {
    x := 10

    closure1 := func() { x += 5 }
    closure2 := func() { fmt.Println("Valor de x:", x) }

    closure1() // x = 15
    closure2() // Valor de x: 15
}

Armadilha comum com loops:

Um erro clássico ocorre ao capturar variáveis de iteração em loops for:

package main

import "fmt"

func main() {
    var funcoes []func()

    // PROBLEMA: todas as closures capturam a mesma variável 'i'
    for i := 0; i < 3; i++ {
        funcoes = append(funcoes, func() {
            fmt.Println(i)
        })
    }

    for _, f := range funcoes {
        f() // Output: 3, 3, 3 (não 0, 1, 2)
    }
}

A solução é criar uma cópia local da variável dentro do loop:

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

4. Closures como Fábricas de Funções

Closures são excelentes para criar fábricas de funções que geram comportamentos personalizados com estado interno.

package main

import "fmt"

// makeAdder retorna uma função que adiciona um valor fixo
func makeAdder(x int) func(int) int {
    return func(y int) int {
        return x + y
    }
}

// makeCounter retorna um contador incremental
func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    // Fábrica de somadores
    add5 := makeAdder(5)
    add10 := makeAdder(10)

    fmt.Println(add5(3))  // 8
    fmt.Println(add10(3)) // 13

    // Fábrica de contadores
    contador1 := makeCounter()
    contador2 := makeCounter()

    fmt.Println(contador1()) // 1
    fmt.Println(contador1()) // 2
    fmt.Println(contador2()) // 1 (contador independente)
    fmt.Println(contador1()) // 3
}

5. Closures e Concorrência (Goroutines)

Closures são frequentemente usadas em goroutines para capturar o contexto necessário.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    // Problema: variável compartilhada em goroutines
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println("Processando", i) // Pode imprimir valores inesperados
        }()
    }

    wg.Wait()
}

Solução com cópia local:

for i := 1; i <= 3; i++ {
    wg.Add(1)
    id := i // Cópia local
    go func() {
        defer wg.Done()
        fmt.Println("Processando", id) // Correto: 1, 2, 3
    }()
}

wg.Wait()

6. Closures em Retornos de Funções e Callbacks

Closures são fundamentais para implementar funções de alta ordem e middlewares.

package main

import (
    "fmt"
    "net/http"
)

// Middleware de logging com closure
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("[LOG] %s %s\n", r.Method, r.URL.Path)
        next(w, r) // Chama o próximo handler
    }
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Olá, mundo!")
}

func main() {
    handler := loggingMiddleware(helloHandler)

    // Simulação de requisição
    req, _ := http.NewRequest("GET", "/hello", nil)
    handler(nil, req) // Output: [LOG] GET /hello
}

7. Boas Práticas e Performance

Evitar captura desnecessária de grandes variáveis:

// Ruim: captura array grande desnecessariamente
func processarGrandeArray() func() {
    dados := make([]byte, 1024*1024) // 1MB
    return func() {
        fmt.Println(len(dados)) // Mantém referência ao array inteiro
    }
}

// Melhor: passar apenas o necessário
func processarTamanho(tamanho int) func() {
    return func() {
        fmt.Println(tamanho)
    }
}

Impacto na alocação de memória:

Closures que capturam variáveis do escopo externo fazem com que essas variáveis sejam alocadas no heap em vez da stack, o que pode impactar a performance do garbage collector.

// Esta closure força 'contador' a ir para o heap
func criaContador() func() int {
    contador := 0
    return func() int {
        contador++
        return contador
    }
}

Quando usar closures vs funções nomeadas vs métodos:

Situação Recomendação
Comportamento simples e único Closure inline
Lógica reutilizável Função nomeada
Estado associado a um tipo Método
Fábrica de funções com estado Closure retornada

Resumo das boas práticas:

  1. Capture apenas as variáveis necessárias
  2. Faça cópias locais de variáveis em loops
  3. Evite closures que modifiquem variáveis externas em goroutines
  4. Prefira funções nomeadas para lógica complexa
  5. Documente o comportamento de captura de variáveis

Referências