Understanding Function Closures in Golang

Function closures have many practical applications (one of which includes isolating data as we will see later in this post). The concept of a function closure (a.k.a closure) can be tricky to understand at first. You may have read that a closure gets its name from the fact that it "closes over" variables that are defined outside its body. You may not, however, feel that you have a complete grasp of this idea. If that is the case for you, fear not! This article will help you grok closures so you are able to effectively work with them in your code.

Before we get into closures, it is helpful to understand what anonymous functions are. An anonymous function is simply a function without a name. In other words, it is a function that can be written on the fly. Let's take a look at some code to understand the difference between a normal function and an anonymous function.

Here is a normal function (named sayHello) as show in the context of a simple Golang program:

package main

import (
        "fmt"
)

func sayHello() {
        fmt.Println("Hello!")
}

func main() {
    sayHello()
}

As you can see from above, sayHello is defined and then called in the main function to print out a greeting.

It is important to remember that we certainly did not have to create a function this way and properly name it in order to use it in our program. We could have done the following and created an anonymous function:

package main

import (
    "fmt"
)

func main() {
    func() { fmt.Println("Hello!") }()
}

Run the code and notice that we will still get our greeting, but this time we haven't actually named the function before calling it. Doing things this way could be useful if you have a bit of code that you know will only need to run once and won't be referenced again. In our case, we will see how this helps us form a closure.

One more thing to remember before moving on is that functions can return values. Let's take a look at the first example of this post and re-write it so that the function returns a string value:

package main

import (
    "fmt"
)

func sayHello() string {
    return "Hello!"
}

func main() {
    fmt.Println(sayHello())
}

So why did we take the time to write the previous function this way? The reason was to not only refresh this basic concept but also to remind you that a string is not the only value that can be returned from a function. Other values that can be returned include integers, booleans, etc. The most important thing for us to remember is that other functions can be returned as well.

Let's take a look at a new example that illustrates the concept discussed above:

func letsCount() func() int {
    magicNumber := 0
    return func() int {
        magicNumber += 3
        return magicNumber
    }
}

What is going on in this snippet of code? First, notice that the letsCount function is returning another function. Keep in mind that this function being returned is also returning something itself (an integer). If we move to the return statement, we can see that it is returning a function that is being anonymously defined. That's right! This function being returned has no name and is being written on the fly. What you should also note is that it has access to magicNumber even though magicNumber was not defined in its body. The reason this is possible is because the anonymous function falls within the scope of letsCount and therefore can access any variables defined above itself.

So why is this useful? Well, keep in mind that letsCount can be called and have the result assigned to another variable (let's call it v1). Since the result being assigned is actually a function (because that is what letsCount returns), we can call v1 multiple times and see that--with each call--v1 will increment magicNumber. In other words, v1 will store the state and update it every time it is called.

Let's look at the full context of the code being discussed:

01:     package main
02:       
03:     import (
04:             "fmt"
05:     )
06: 
07:     func letsCount() func() int {
08:             magicNumber := 0
09:             return func() int {
10:                     magicNumber += 3
11:                     return magicNumber
12:             }
13:     }
14: 
15:     func main() {
16:             v1 := letsCount()
17:             fmt.Println(v1())
18:             fmt.Println(v1())
19:     }

If you run this code, you will get the following results:

$ go run closure.go
3
6

Let's see why this makes sense. Note that letsCount was assigned to v1 on line 16. We then called v1 on line 17 (therefore incrementing magicNumber by 3) and printed the results. v1 was then called and printed a second time on line 18 (incrementing magicNumber by 3 again).

Keep in mind that v1 is holding its own state and will update magicNumber appropriately every time it is called. You can create v2 and then assign it letsCount as well. In this case v2 will hold its own state separate from v1 and will update it accordingly every time it is called. The following code has been added to illustrate this concept.

01:     package main
02: 
03:     import (
04:         "fmt"
05:     )
06: 
07:     func letsCount() func() int {
08:         magicNumber := 0
09:         return func() int {
10:             magicNumber += 3
11:             return magicNumber
12:         }
13:     }
14: 
15:     func main() {
16:         v1 := letsCount()
17:         fmt.Println(v1())
18:         fmt.Println(v1())
19: 
20:         v2 := letsCount()
21:         fmt.Println(v2())
22:     }

Running the code above will give you the following results:

$ go run closure.go 
3
6
3

Keep in mind that v2 started off fresh with its own state and therefore produces 3 when called once. If you continue to call v2, you will see that it will start incrementing magicNumber independent of v1. In this way, you can think of a closure being used to isolate data (although there are many other situations where you can use closures as well).

I hope this has been helpful. If you are interested in keeping up with my latest content, you can follow me on Twitter.