Closures
Functions are values; a function literal that uses an outer variable shares it, which is both the point and the trap.
next := func() int { n++; return n }In Go a function is a value, like a number or a string. You can store one in a variable, pass one to another function, and hand one back from a function. A function written without a name, func(x int) int { return x * 2 }, is a function literal. Its type is written func(int) int: takes an int, returns an int.
A function literal may use variables belonging to the function that contains it. When it does, those variables stay alive as long as the literal does, even after the outer function has finished. A literal that holds on to variables this way is a closure. It closes over them.
It captures the variable, not the value
That sentence is the whole subject. The closure and the surrounding code share one variable. If the outer code changes it, the closure sees the new value, and two closures over the same variable see each other's changes. It is also what makes a counter possible: the count lives in a variable that only the returned function can reach, with no package-level variable and no struct.
The loop surprise
Before Go 1.22, a for loop created its variables once and updated them on each pass. Every closure made in the body shared one i, so all of them ended up seeing the final value. Go 1.22 changed that: each iteration now creates new variables. Older code often has i := i at the top of the body, a deliberate copy, and that line is no longer needed. A variable declared outside the loop is still shared by every closure, and still a trap.
Example
package main
import "fmt"
// makeCounter hands back a function value. The returned function keeps
// using n, declared here, after makeCounter has already finished.
// Sharing that variable is what makes it a closure.
func makeCounter() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
hits := makeCounter()
fmt.Println(hits())
fmt.Println(hits())
fmt.Println(hits())
// A second call to makeCounter gets its own n.
other := makeCounter()
fmt.Println(other())
}
// Output:
// 1
// 2
// 3
// 1When to use it
- Keep a running count or total in a function without a package-level variable.
- Pass one small step to a helper, such as how to format each line it writes.
- Hand back a pre-configured function so callers can invoke it with no arguments.
More examples
Inside the loop versus outside it
Two variables, one closure, two different outcomes, which is the clearest way to see what capture means. Before Go 1.22 both would have behaved like shared and the answer would have been 7. Reach for this test whenever you save a function for later inside a loop: ask which variables it names, and where each of them was declared.
package main
import "fmt"
func main() {
var saved func() int
// i is a new variable on every pass since Go 1.22.
// shared is declared once, before the loop, so every closure
// over it sees whatever it holds when the closure is called.
shared := 0
for i := 1; i <= 3; i++ {
shared = i
if i == 1 {
saved = func() int { return i + shared }
}
}
// The saved closure kept the first pass's i, which is 1.
// shared is one variable and the loop left it at 3.
fmt.Println(saved())
}
// Output:
// 4Passing behaviour to a function
A function parameter lets one helper handle the structure of a job while the caller supplies the one step that differs: how to format a line, how to compare two records, what to retry. Writing strings.ToUpper without brackets passes the function itself; adding brackets would call it. The literal shows why closures matter here, since it carries sep in without twice needing a parameter for it.
package main
import (
"fmt"
"strings"
)
// twice calls f on s, then on the result. The parameter type
// func(string) string means "a function taking a string,
// returning a string".
func twice(s string, f func(string) string) string {
return f(f(s))
}
func main() {
// A named function is passed by name, with no brackets after it.
fmt.Println(twice("ab", strings.ToUpper))
// A function literal does the same job without a name. This one
// captures sep, which twice knows nothing about.
sep := "-"
fmt.Println(twice("ab", func(s string) string { return s + sep }))
}
// Output:
// AB
// ab--
Discussion