Lautaro Strappazzon
Posted on February 21, 2024
Introducción
Las funciones anónimas (funciones lambda o funciones literales) son aquellas que no están vinculadas a un identificador. En Golang son principalmente usadas para diferir tareas o ejecutarlas concurrentemente; para pasar funciones como argumentos, entre otros casos de uso que vamos a cubrir hoy.
Casos de uso
- Funciones como argumentos
Cuando una función de orden superior tiene como parámetro otra función, es usual ver que funciones anónimas son pasadas como parámetros a estas. A esto también se le llama “callbacks”.
func forEach(numbers []int, f func(int)) {
for _, num := range numbers {
f(num)
}
}
func main() {
forEach([]int{1, 2, 3}, func(i int) {
fmt.Printf("%d ", i * i) // Output: 1 4 9
})
}
Ejecuta el código:
- Ejecuciones concurrentes (Goroutines)
Otro uso común es a la hora de lanzar Goroutines, sobre todo cuando la función es simple y sólo se utiliza en ese lugar. Esto nos permite mantener la lógica cerca de donde es utilizada, mejorando la legibilidad.
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
// Código que se ejecuta concurrentemente
fmt.Println("Ejecutando en una goroutine")
wg.Done()
}()
wg.Wait()
fmt.Println("Ejecutando en la función main")
}
Ejecuta el código:
ADVERTENCIA: Funciones anónimas muy largas pueden generar el efecto contrario en la legibilidad, haciendo difícil entender el código.
- Diferir tareas
Cuando queremos diferir la ejecución de más de una función en general utilizamos funciones anónimas.
func main() {
now := time.Now()
defer func() {
duration := time.Since(now)
fmt.Printf("Se tardó %v en ejecutar esto", duration)
}()
for i := range 1_001 {
if i%100 == 0 {
fmt.Printf("%d ", i)
time.Sleep(time.Millisecond * 100)
}
}
fmt.Printf("\n")
}
// Output:
// 0 100 200 300 400 500 600 700 800 900 1000
// Se tardó 1.1s en correr esto
Ejecuta el código:
- Clausuras o closures
Las funciones anónimas pueden capturar variables dentro de su scope y mantener acceso a ellas aún cuando la función exterior o padre ya ha retornado. Esto nos permite crear funciones que “recuerdan” estado.
func makeMultiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
func main() {
multiplyByTwo := makeMultiplier(2)
r1 := multiplyByTwo(2)
fmt.Println(r1) // Output: 4
r2 := multiplyByTwo(5)
fmt.Println(r2) // Output: 10
}
Ejecuta el código:
ADVERTENCIA: A pesar de su potencial, hay que saber cuándo y cómo utilizar closures porque pueden traer problemas de uso de memoria, de legibilidad; bugs a la hora de manejar ese “estado”, sobre todo concurrentemente; o dificultades a la hora de debuggear.
- Testing
Solemos usar funciones anónimas para correr “sub tests” en Table-Driven Tests:
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
testCases := []struct {
a, b, expected int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%d+%d", tc.a, tc.b), func(t *testing.T) {
result := Add(tc.a, tc.b)
if result != tc.expected {
t.Errorf("Expected %d, got %d", tc.expected, result)
}
})
}
}
Volviendo a las clausuras o closures, el siguiente es un patrón que le robé a Mat Ryer en una de sus charlas en GopherCon UK que recomiendo enormemente.
func TestSomething(t *testing.T) {
file, teardown, err := setup(t)
defer teardown()
if err != nil {
t.Error("setup:", err)
}
// hacer algo con el archivo
}
func setup(t *testing.T) (*os.File, func(), error) {
teardown := func() {}
// crear archivo de test
file, err := os.CreateTemp(os.TempDir(), "test")
if err != nil {
return nil, teardown, err
}
teardown = func() {
// cerrar archivo
err := file.Close()
if err != nil {
t.Error("setup: Close:", err)
}
// borrar archivo de prueba
err = os.RemoveAll(file.Name())
if err != nil {
t.Error("setup: RemoveAll:", err)
}
}
return file, teardown, nil
}
Pros y contras
- Pros:
- Concisión: Permiten definir y usar funciones directamente donde las necesitas, lo que reduce el desorden en tu código y el tamaño de tus APIs internas o privadas.
- Legibilidad (para tareas simples): las funciones anónimas pueden hacer el código más fácil de leer manteniéndolo cerca de donde se utiliza.
- Contras:
- Testabilidad y debugging: al no estar explicitamente definidas y nombradas, pueden ser más difíciles de testear y debuggear.
- Legibilidad (para tareas largas o complejas): cuando esto sucede, el código puede ser más difícil de comprender.
Conclusiones
Las funciones anónimas pueden ser una herramienta muy poderosa, pero hay que elegir cuidadosamente cuando las utilizamos. En resumen, puedes utilizarlas cuando su lógica es simple y corta, y no va a ser reutilizada; y/o cuando necesites hacer uso de closures.
En los casos en los que su lógica se vuelve muy compleja o la función va a ser reutilizada en otros lugares, considera definirla como una función nombrada para mayor mantenibilidad y claridad.
Posted on February 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.