Lautaro Strappazzon
Posted on February 21, 2024
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
})
}
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")
}
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
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
}
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)
}
})
}
}
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
}
Pros and Cons
- Pros:
- 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.
- Readability (for simple tasks): Anonymous functions can make the code easier to read by keeping it close to where it is used.
- Cons:
- Testability and debugging: As they are not explicitly defined and named, they can be more challenging to test and debug.
- 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.
Posted on February 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.