In a previous article on testing with Go there is an example that uses a closure to create table-driven tests. This is a fairly common practice in Go, but after writing the article a developer reached out to me and made the following comment.
“I learned something. Mainly that I have no idea how Go closures work”
Ouch! That means that despite the rest of the article being packed with great content on testing, if a reader didn’t understand closures they weren’t going to get the full benefit of the article.
This post is intended to fix that problem. In it, we are going to discuss what closures are and why you should care about them. Then in a followup article, 5 Useful Ways to Use Closures in Go (redundant, I know), we will explore some of the more common use cases for closures to help give you ideas of where and when to use closures in your own code. Each use case has a specific example that I have encountered, so they aren’t just imaginary uses.
Before we jump into closures, we need to first talk about anonymous functions. An anonymous function is the same as a regular old function, but it doesn’t have a name - hence the term “anonymous”. Instead, an anonymous function is created dynamically, much like a variable is.
Like most things, this can be easier to explain with code. Below is what a normal function would look like.
func DoStuff() {
// Do stuff
}
If we wanted to turn this same function into an anonymous function, we wouldn’t declare it quite the same way; rather than starting with the func
keyword, we could instead create a variable that has a func()
type. After doing this, we could create and assign an anonymous function to he variable.
var DoStuff func() = func() {
// Do stuff
}
There are a lot of subtle differences between these two, but for most practical purposes, the big difference is that we could assign a new function to the DoStuff
variable at runtime, allowing us to dynamically change what DoStuff()
does.
package main
import "fmt"
var DoStuff func() = func() {
// Do stuff
}
func main() {
DoStuff()
DoStuff = func() {
fmt.Println("Doing stuff!")
}
DoStuff()
DoStuff = func() {
fmt.Println("Doing other stuff.")
}
DoStuff()
}
If you run this program you would see the following output.
Doing stuff!
Doing other stuff.
We are seeing two different lines of output because we declared three different anonymous functions, and the last two each are printing out something different.
Anonymous functions can accept parameters, return data, and do pretty much anything else a normal function can do. You can even assign a regular function to a variable just like you do with an anonymous function.
package main
import "fmt"
var DoStuff func() = func() {
// Do stuff
}
func RegFunc() { fmt.Println("reg func") }
func main() {
DoStuff()
DoStuff = RegFunc
DoStuff()
}
The only real difference between a regular function and an anonymous one is that anonymous functions aren’t declared at a package level. They are declared more dynamically and are typically either used and then forgotten or are assigned to a variable for later use.
A closure is a special type of anonymous function that references variables declared outside of the function itself. That means that like in the previous examples, we will be creating a function dynamically, but in this case we will be using variables that weren’t passed into the function as a parameter, but instead were available when the function was declared.
This is very similar to how a regular function can reference global variables. You aren’t directly passing these variables into the function as a parameter, but the function has access to them when it is called.
Let’s take a look at a closure in action. In this next example we are going to create a closure that keeps track of how many times it has been called, and returns that number.
package main
import "fmt"
func main() {
n := 0
counter := func() int {
n += 1
return n
}
fmt.Println(counter())
fmt.Println(counter())
}
If you were to run this code you would get the output:
1
2
Notice how our anonymous function has access to the n
variable, but this was never passed in as a parameter when counter()
was called. This is what makes it a closure!
One problem with the previous example is a problem that can also pop up when using global variables. Any code inside of the main()
function has access to n
, so it is possible to increment the counter without actually calling counter()
. That isn’t what we want; Instead we would rather have n
isolated so that no other code has access to it.
To do this we need to look at another interesting aspect of closures, which is the fact that they can still reference variables that they had access to during creation even if those variables are no longer referenced elsewhere.
package main
import "fmt"
func main() {
counter := newCounter()
fmt.Println(counter())
fmt.Println(counter())
}
func newCounter() func() int {
n := 0
return func() int {
n += 1
return n
}
}
In this example our closure references the variable n
even after the newCounter()
function has finished running. This means that our closure has access to a variable that keeps track of how many times it has been called, but no other code outside of the newCounter()
function has access to this variable. This is one of the many benefits of a closure - we can persist data between function calls while also isolating the data from other code.
Want to improve your Go skills?
Are you looking to practice Go, but can’t think of a good project to work on? Or maybe you just don’t know what techniques and skills you need to learn next, so you don't know where to start. If so, don’t worry - I’ve got you covered!
Gophercises is a FREE course where we work on exercise problems that are each designed to teach you a different aspect of Go. This includes topics ranging from basic string manipulation all the way to more advanced topics like functional options and concurrency. Each exercise has a sample solution, as well as a screencast (video) where I code the solution while walking you through the code. Plus, the Gophers are really cute 😉
In my next post - - I go over several common use cases for closures, including ways that closures can make code easier to read and understand, how closures can prevent the callback hell that rears its head so often in javascript, and even how you can use closures to create middleware that times your web applications execution time. You should check it out so you can easily recognize situations where closures can be helpful!
Following that I will have a small another article dedicated to gotchas and common mistakes that developers make when creating closures, and how to avoid them.
This article is part of the series, Closures in Go.
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
More in this series
This post is part of the series, Closures in Go.
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.