Estilo funcional em Go
Nesta aula, exploramos o estilo funcional em Go, abordando funções de ordem superior, closures e implementações manuais de map/filter, além de discutir as limitações da linguagem para esse paradigma. Ao final, você será capaz de aplicar esses conceitos para escrever código mais expressivo e reutilizável.
O estilo funcional é um paradigma de programação que trata a computação como a avaliação de funções matemáticas e evita mudanças de estado e dados mutáveis. Em Go, embora a linguagem não seja puramente funcional, ela oferece suporte a vários conceitos funcionais, como funções de ordem superior, closures e funções anônimas. Nesta aula, vamos explorar como aplicar esses conceitos em Go, entender suas vantagens e limitações, e implementar funções utilitárias como map e filter manualmente.
Go é uma linguagem imperativa por natureza, mas sua sintaxe flexível permite adotar um estilo funcional em muitos casos. Isso pode tornar o código mais conciso, testável e modular. No entanto, é importante conhecer as restrições, como a ausência de genéricos (antes da versão 1.18) e a impossibilidade de ter funções como valores de retorno em alguns contextos, para usar o estilo funcional de forma eficaz.
Funções de ordem superior
Funções de ordem superior são funções que recebem outras funções como argumentos ou retornam funções como resultado. Em Go, funções são valores de primeira classe, o que significa que podem ser atribuídas a variáveis, passadas como parâmetros e retornadas de outras funções. Isso permite criar abstrações poderosas, como funções de callback, decoradores e pipelines.
Vamos ver um exemplo simples de uma função de ordem superior que aplica uma operação a cada elemento de um slice. Suponha que queremos uma função apply que recebe um slice de inteiros e uma função que transforma um inteiro em outro, e retorna um novo slice com os resultados.
package main
import "fmt"
// Função de ordem superior: recebe uma função como parâmetro
func apply(slice []int, f func(int) int) []int {
result := make([]int, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
func main() {
nums := []int{1, 2, 3, 4}
// Passando uma função anônima (closure)
doubled := apply(nums, func(x int) int { return x * 2 })
fmt.Println(doubled) // [2 4 6 8]
}
Nesse exemplo, a função apply é de ordem superior porque recebe a função f como argumento. Essa abordagem permite reutilizar a lógica de iteração e variar a transformação. Além disso, podemos passar funções nomeadas ou closures. Isso é especialmente útil para operações comuns como mapear, filtrar e reduzir.
Outra característica é retornar funções. Por exemplo, podemos criar uma função que gera uma função de incremento com um passo específico:
func adder(step int) func(int) int {
return func(x int) int {
return x + step
}
}
func main() {
add5 := adder(5)
fmt.Println(add5(10)) // 15
}
Isso é útil para criar funções personalizadas em tempo de execução, como fábricas de funções.
Closures
Closures são funções que capturam variáveis de seu escopo léxico. Em Go, toda função anônima é um closure, pois pode referenciar variáveis definidas fora da função. Isso permite criar funções que mantêm estado interno, mesmo após a função externa ter retornado.
Um exemplo clássico é um contador:
func contador() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
c := contador()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
}
Aqui, a variável count é capturada pelo closure e persiste entre chamadas. Isso é útil para criar funções com estado sem usar variáveis globais, promovendo encapsulamento.
Outro uso comum de closures é em callbacks, como em operações assíncronas ou manipulação de eventos. Por exemplo, podemos usar closures para capturar valores em loops, evitando problemas de variáveis de iteração compartilhadas (embora a partir do Go 1.22 isso tenha sido corrigido, ainda é uma boa prática entender).
É importante lembrar que closures capturam variáveis por referência, não por valor. Isso significa que se você capturar uma variável de loop, todas as closures podem ver o valor final da variável. Vamos ver um exemplo:
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: imprime 3 3 3
// Depois do Go 1.22: imprime 0 1 2
}
Para evitar esse comportamento em versões anteriores, é comum criar uma variável local dentro do loop:
for i := 0; i < 3; i++ {
i := i // cria uma nova variável a cada iteração
funcs = append(funcs, func() {
fmt.Println(i)
})
}
Closures são uma ferramenta poderosa, mas exigem cuidado com o ciclo de vida das variáveis capturadas para evitar efeitos inesperados.
map/filter manuais
Em linguagens funcionais, map e filter são funções de alta ordem que operam sobre coleções. Em Go, não há implementação nativa para slices, então podemos criar nossas próprias funções genéricas (usando type parameters) ou específicas para tipos concretos. Vamos implementar map e filter para slices de inteiros, e depois generalizar com genéricos.
Implementação específica:
func mapInts(slice []int, f func(int) int) []int {
result := make([]int, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
func filterInts(slice []int, f func(int) bool) []int {
var result []int
for _, v := range slice {
if f(v) {
result = append(result, v)
}
}
return result
}
Exemplo de uso:
func main() {
nums := []int{1, 2, 3, 4, 5}
doubled := mapInts(nums, func(x int) int { return x * 2 })
evens := filterInts(nums, func(x int) bool { return x%2 == 0 })
fmt.Println(doubled) // [2 4 6 8 10]
fmt.Println(evens) // [2 4]
}
Versão genérica (Go 1.18+):
func Map[T any](slice []T, f func(T) T) []T {
result := make([]T, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
func Filter[T any](slice []T, f func(T) bool) []T {
var result []T
for _, v := range slice {
if f(v) {
result = append(result, v)
}
}
return result
}
Com genéricos, podemos usar essas funções com qualquer tipo de slice. Isso torna o código mais reutilizável e próximo do estilo funcional.
Também podemos combinar map e filter em pipelines, como em Filter(Map(...)), mas é importante notar que cada operação cria um novo slice, o que pode ter custo de memória. Para grandes coleções, pode ser mais eficiente usar loops tradicionais.
Outra função comum é reduce (ou fold), que combina elementos em um único valor. Vamos implementar também:
func Reduce[T any](slice []T, initial T, f func(T, T) T) T {
acc := initial
for _, v := range slice {
acc = f(acc, v)
}
return acc
}
Exemplo:
sum := Reduce(nums, 0, func(a, b int) int { return a + b })
fmt.Println(sum) // 15
Essas funções manuais são úteis, mas é importante considerar que a biblioteca padrão do Go não as fornece, então você precisa manter seu próprio código ou usar bibliotecas de terceiros, como github.com/samber/lo.
Limites
Embora Go suporte muitos conceitos funcionais, existem limitações que devemos conhecer:
- Ausência de genéricos antes da versão 1.18: era necessário escrever funções específicas para cada tipo, o que gerava duplicação de código.
- Sem imutabilidade garantida: slices e mapas são mutáveis por padrão, então é fácil alterar dados acidentalmente. O estilo funcional puro prefere estruturas imutáveis, mas Go não oferece isso nativamente.
- Sem recursão de cauda otimizada: Go não otimiza recursão de cauda, então recursão profunda pode estourar a pilha. É preferível usar iteração.
- Funções como valores são limitadas: não é possível comparar funções com
==, e não há suporte a curry nativo. - Ausência de tipos algébricos de dados: não há enums como em Haskell ou Rust, o que dificulta certos padrões funcionais.
- Performance: o uso excessivo de closures e funções de alta ordem pode ter overhead, embora o compilador otimize bem.
Apesar dessas limitações, é possível adotar um estilo funcional pragmático em Go, combinando com a programação imperativa. Muitas bibliotecas populares, como lo e go-funk, oferecem utilitários funcionais para facilitar.
Boas práticas
Ao usar estilo funcional em Go, considere:
- Use closures para encapsular estado e evitar variáveis globais.
- Prefira funções puras (sem efeitos colaterais) para facilitar testes e manutenção.
- Combine map/filter/reduce para operações de coleção, mas avalie a legibilidade e o desempenho.
- Evite closures em loops se não for necessário, para não capturar variáveis por referência indevidamente.
- Documente funções de alta ordem com exemplos, pois podem ser menos óbvias.
Referências
- Go Blog: Functions
- Go Blog: Generics
- Go by Example: Closures
- Go by Example: Higher-order functions
- Go standard library: slices
- samber/lo: Functional helpers for Go
Exercícios
Crie uma função de ordem superior chamada
composeque recebe duas funçõesfege retorna uma nova função que aplicagprimeiro e depoisf(ou seja,f(g(x))). Teste com funções simples.✓ Resposta:func compose(f func(int) int, g func(int) int) func(int) int { return func(x int) int { return f(g(x)) } } func main() { add1 := func(x int) int { return x + 1 } double := func(x int) int { return x * 2 } h := compose(add1, double) fmt.Println(h(5)) // 11 (add1(double(5)) = 5*2+1) }Implemente uma função
contadorComPassoque retorna um closure que incrementa um valor interno por um passo dado. O passo deve ser fixo na criação.✓ Resposta:func contadorComPasso(passo int) func() int { count := 0 return func() int { count += passo return count } } func main() { cont := contadorComPasso(3) fmt.Println(cont()) // 3 fmt.Println(cont()) // 6 }Escreva uma função
filterMapque recebe um slice de inteiros, uma função de filtro e uma função de mapeamento, e retorna um novo slice com os elementos que passam no filtro e depois são mapeados. Use as funções que você criou.✓ Resposta:func filterMap(slice []int, filtro func(int) bool, mapear func(int) int) []int { var result []int for _, v := range slice { if filtro(v) { result = append(result, mapear(v)) } } return result } func main() { nums := []int{1, 2, 3, 4, 5} result := filterMap(nums, func(x int) bool { return x%2 == 0 }, func(x int) int { return x * 10 }) fmt.Println(result) // [20 40] }Explique por que o seguinte código imprime 5 5 5 em Go antes da versão 1.22 e sugira uma correção.
func main() { for i := 0; i < 3; i++ { defer func() { fmt.Println(i) }() } }✓ Resposta: O código imprime 5 5 5 porque a variávelié capturada por referência pelo closure, e quando as funçõesdefersão executadas no final da funçãomain, o valor deijá é 5 (após o loop terminar). Para corrigir, capture o valor atual da variável a cada iteração, por exemplo:for i := 0; i < 3; i++ { i := i defer func() { fmt.Println(i) }() }Crie uma função genérica
Aplicarque recebe um slice de qualquer tipo e uma função que transforma cada elemento, e retorna um novo slice. Teste com um slice de strings e uma função que converte para maiúsculas.✓ Resposta:func Aplicar[T any](slice []T, f func(T) T) []T { result := make([]T, len(slice)) for i, v := range slice { result[i] = f(v) } return result } func main() { palavras := []string{"go", "é", "legal"} maiusculas := Aplicar(palavras, func(s string) string { return strings.ToUpper(s) }) fmt.Println(maiusculas) // [GO É LEGAL] }