Golang Anonymous Functions: A Guide [#Go101]

lautistr

Lautaro Strappazzon

Posted on February 21, 2024

Golang Anonymous Functions: A Guide [#Go101]

Introduction

Anonymous functions (lambda functions or literal functions) are those that are not bound to an identifier. In Golang, they are primarily used to defer tasks or execute them concurrently; to pass functions as arguments, among other use cases that we will cover today.

Use Cases

  • Functions as Arguments

When a higher-order function takes another function as a parameter, it is common to see anonymous functions passed as parameters to these. This is also called "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

Run this code:

  • Concurrent Executions (Goroutines)

Another common use is when launching Goroutines, especially when the function is simple and only used in that place. This allows us to keep the logic close to where it is used, improving readability.

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        // Code running concurrently
        fmt.Println("Running in a goroutine")
        wg.Done()
    }()
    wg.Wait()

    fmt.Println("Running in the main function")
}
Enter fullscreen mode Exit fullscreen mode

Run this code:

WARNING: Very long anonymous functions may have the opposite effect on readability, making it challenging to understand the code.

  • Deferring Tasks

When we want to defer the execution of more than one function, we generally use anonymous functions.

func main() {
    now := time.Now()
    defer func() {
        duration := time.Since(now)
        fmt.Printf("It took %v to run this", 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
// It took 1.1s to run this
Enter fullscreen mode Exit fullscreen mode

Run this code:

  • Closures

Anonymous functions can capture variables within their scope and maintain access to them even when the outer or parent function has already returned. This allows us to create functions that "remember" state.

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

Run this code:

WARNING: Despite their potential, one must know when and how to use closures because they can bring issues of memory usage, readability, bugs when handling that "state," especially concurrently, or difficulties in debugging.

  • Testing

We often use anonymous functions to run "subtests" in 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

Returning to closures, the following is a pattern I borrowed from Mat Ryer in one of his talks at GopherCon UK that I highly recommend.

func TestSomething(t *testing.T) {
    file, teardown, err := setup(t)
    defer teardown()
    if err != nil {
        t.Error("setup:", err)
    }
    // do something with the file
}

func setup(t *testing.T) (*os.File, func(), error) {
    teardown := func() {}
    // create test file
    file, err := os.CreateTemp(os.TempDir(), "test")
    if err != nil {
        return nil, teardown, err
    }

    teardown = func() {
        // close file
        err := file.Close()
        if err != nil {
            t.Error("setup: Close:", err)
        }
        // remove test file
        err = os.RemoveAll(file.Name())
        if err != nil {
            t.Error("setup: RemoveAll:", err)
        }
    }
    return file, teardown, nil
}

Enter fullscreen mode Exit fullscreen mode

Pros and Cons

  • Pros:
  1. Conciseness: Allows you to define and use functions directly where you need them, reducing clutter in your code and the size of your internal or private APIs.
  2. Readability (for simple tasks): Anonymous functions can make the code easier to read by keeping it close to where it is used.
  • Cons:
  1. Testability and debugging: As they are not explicitly defined and named, they can be more challenging to test and debug.
  2. Readability (for long or complex tasks): When this happens, the code can become harder to understand.

Conclusions

Anonymous functions can be a powerful tool, but one must choose carefully when to use them. In summary, you can use them when their logic is simple and short, and it will not be reused; and/or when you need to make use of closures.

In cases where their logic becomes very complex or the function will be reused elsewhere, consider defining it as a named function for greater maintainability and clarity.

💖 💪 🙅 🚩
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