Funciones Anónimas en Golang [#Go101]

lautistr

Lautaro Strappazzon

Posted on February 21, 2024

Funciones Anónimas en Golang [#Go101]

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
    })
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

Pros y contras

  • Pros:
  1. 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.
  2. 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:
  1. Testabilidad y debugging: al no estar explicitamente definidas y nombradas, pueden ser más difíciles de testear y debuggear.
  2. 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.

💖 💪 🙅 🚩
lautistr
Lautaro Strappazzon

Posted on February 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related