notexOPENOverview
Notex/Languages/Closures
03.11LESSON 11 OF 18 · 7 MIN READ

Closures

Always free to read

Functions declared inside functions are special; they are called closures. This is a computer-science word that means that functions declared inside functions can access and modify variables declared in the outer function. Let’s look at a quick example to see how this works. You can find the code on The Go Playground. The code is also in the examples/closure directory:

func main() {
    a := 20
    f := func() {
        fmt.Println(a)
        a = 30
    }
    f()
    fmt.Println(a)
}

Running this program gives the following output:

20
30

The anonymous function assigned to f can read and write a, even though a is not passed into it.

Just as with any inner scope, you can shadow a variable inside a closure. Try changing the code to:

func main() {
    a := 20
    f := func() {
        fmt.Println(a)
        a := 30
        fmt.Println(a)
    }
    f()
    fmt.Println(a)
}

This will print out the following:

20
30
20

Using := instead of = inside the closure creates a new a that ceases to exist when the closure exits. When working with inner functions, be careful to use the correct assignment operator, especially when multiple variables are on the left-hand side. You can try this code on The Go Playground. The code is also in the examples/closure_shadow directory.

This inner function and closure stuff might not seem all that useful at first. What benefit do you get from making mini-functions within a larger function? Why does Go have this feature?

One thing that closures allow you to do is to limit a function’s scope. If a function is going to be called from only one other function, but it’s called multiple times, you can use an inner function to “hide” the called function. This reduces the number of declarations at the package level, which can make it easier to find an unused name.

If you have a piece of logic that is repeated multiple times within a function, you can use a closure to remove that repetition. For example, a simple Lisp interpreter can use a Scan function that processes an input string to find the parts of a Lisp program. It relies on two closures, buildCurToken and update, to make the code shorter and easier to understand.

Closures really become interesting when they are passed to other functions or returned from a function. They allow you to take the variables within your function and use them outside of your function.

Passing Functions as Parameters

Since functions are values and you can specify function’s type using its parameter and return types, you can pass functions as parameters into functions. If you aren’t used to treating functions like data, take a moment to think about the implications of creating a closure that references local variables and then passing that closure to another function. It’s a very useful pattern that appears several times in the standard library.

The slices and maps packages have many functions with function parameters. In “Comparing Slices” and “Comparing Maps”, you learned about slices.EqualFunc and maps.EqualFunc, which use a function parameter to test equality for elements in the containers.

Another example is sorting slices. The slices package in the standard library has a function called slices.Sort that takes in a slice and sorts the values in it. However, it only works for slices whose elements are of an ordered type, and it only sorts thw values in ascending order. (As a reminder, an ordered type is one that can be compared with >, <, >=, and ⇐.)

If you want to sort a slice that contains elements of a nonordered type, or if you want to specify your own sort order, use the slices.SortFunc function. Let’s see how it works by sorting a slice of a struct with multiple fields. Here’s the sample data:

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

people := []Person{
    {"Pat", "Patterson", 37},
    {"Tracy", "Bobdaughter", 23},
    {"Fred", "Fredson", 18},
    {"Fred", "Fredson", 92},
    {"Mary", "Patterson", 42},
}

You can’t use slices.Sort to sort this slice, because Person isn’t an ordered type. Here is the code to sort this slice by last name in ascending alphabetical order:

slices.SortFunc(people, func(p1, p2 Person) int {
    switch {
    case p1.LastName < p2.LastName:
        return -1
    case p1.LastName > p2.LastName:
        return 1
    default:
        return 0
    }
})

Note

The people slice is changed by the call to slices.SortFunc. I talk about this briefly in “Go Is Call by Value” and in more detail in this topic.

SortFunc takes in a slice and a compare function. A compare function takes in two parameters of the same type and returns an int. It returns a value less than 0 when the first parameter is ordered before the second parameter, 0 when the parameters are considered identical, and greater than 0 when the second parameter is ordered before the first parameter.

If you are building a comparison function out of orderable types, the Go standard library contains a useful helper function. While Person isn’t orderable, its LastName field is. Call the cmp.Compare function with the fields you are comparing to remove a lot of ugly boilerplate code:

slices.SortFunc(people, func(p1, p2 Person) int {
    return cmp.Compare(p1.LastName, p2.LastName)
})

Here are few more sorting variations. To sort by last name, descending, swap the order of the parameters to cmp.Compare:

slices.SortFunc(people, func(p1, p2 Person) int {
    return cmp.Compare(p2.LastName, p1.LastName)
})

What if you want to sort by multiple fields? The cmp package includes another useful function called Or. It takes in zero or more values and returns the first nonzero value. If all of the values are 0, cmp.Or returns 0.

Combining cmp.Or with cmp.Compare allows you to list multiple compare functions. The first nonzero value determines the order of the two values. If all of the compare functions return 0, the values are equal. Here is how to use it to sort by last name, first name, and age:

slices.SortFunc(people, func(p1, p2 Person) int {
    return cmp.Or(
        cmp.Compare(p1.LastName, p2.LastName),
        cmp.Compare(p1.FirstName, p2.FirstName),
        cmp.Compare(p1.Age, p2.Age),
    )
})

What if you want a configurable sort function? You can take advantage of closures’ ability to capture a local variable (which is the official computer-science way to say that closures can access local variables) and pass a function to slices.SortFunc that reads the value of function parameters to determine how to sort them:

func PeopleSorter(people []Person, sortBy string, direction string) {
    slices.SortFunc(people, func(p1, p2 Person) int {
        var out int
        switch sortBy {
        case "lastname":
            out = cmp.Compare(p1.LastName, p2.LastName)
        case "firstname":
            out = cmp.Compare(p1.FirstName, p2.FirstName)
        case "age":
            out = cmp.Compare(p1.Age, p2.Age)
        default:
            out = 0 // don't sort
        }
        if direction == "desc" {
            out = -out
        }
        return out
    })
}

Tip

Passing functions as parameters to other functions is often useful for performing different operations on the same kind of data.

You can try out this code on the Go Playground.

One common pattern in the standard library is pairing Sort with SortFunc: a function that provides a standard behavior for a specific set of types, and a Func variant that has a function parameter to support additional types and implement customized behavior. You’ve already seen slices.Equal with slices.EqualFunc and slices.Compare with slices.CompareFunc. There are more of these pairs in the slices package, including Contains/ContainsFunc, Delete/DeleteFunc, and Min/MinFunc.

Note

All of the functions I’ve used in these examples (slices.Sort, slices.SortFunc, cmp.Compare, and cmp.Or) take advantage of Go’s generics support. I’ll cover generics in detail in the related discussion.

Returning Functions from Functions

In addition to using a closure to pass some function state to another function, you can also return a closure from a function. Let’s demonstrate this by writing a function that returns a multiplier function. You can run this program on The Go Playground. The code is also in the examples/makeMult directory. Here is a function that returns a closure:

func makeMult(base int) func(int) int {
    return func(factor int) int {
        return base * factor
    }
}

And here is how the function is used:

func main() {
    twoBase := makeMult(2)
    threeBase := makeMult(3)
    for i := 0; i < 3; i++ {
        fmt.Println(twoBase(i), threeBase(i))
    }
}

Running this program gives the following output:

0 0
2 3
4 6

Now that you’ve seen closures in action, you might wonder how often Go developers use them. As it turns out, they’re surprisingly useful. The slices and maps packages are mostly composed of functions that take in functions or return functions. The slices.Values function and the other functions I covered in “Iterating over functions” aren’t iterator functions themselves; they return closures that implement the iterator function types. You will see this pattern when you build middleware for a web server in the related discussion. Go also uses closures to implement resource cleanup, via the defer keyword.

Note

If you spend any time with programmers who use functional programming languages like Haskell, you might hear the term higher-order functions. That’s a fancy way to say that a function has a function for an input parameter or a return value. As a Go developer, you are as cool as they are!

Progress is saved on this device.
KEEP EXPLORING

One note leads to another.

Back to the library →
⌕
Search across the entire library ⌘ K