Functions as Values
Functions as Values
A function value is data. You can pass it, store it, and return it. The boring default is a named function or a tiny literal at the call site — the same idea as an HTTP handler, without a server.
Mental model
Every function has a type: the parameter types and the result types. func(int) bool is a type, the way int is a type. A variable of that type holds a function, or it is nil.
A closure is a function literal that uses variables from the surrounding function. Those variables are shared, not copied: later assignments are visible when the closure runs. That is the power and the leak.
Since Go 1.22 (and in this book’s Go 1.27), each iteration of a for loop has its own per-iteration variables. Closures that capture name inside for _, name := range names see that iteration’s name, not the last one. You still have to think when you close over a variable outside the loop.
Worked examples
Case 1: A function type as a parameter
Save as keep.go. keep does not know what “interesting” means. The caller passes a predicate.
// keep.go
package main
import "fmt"
func keep(orders []int, ok func(int) bool) []int {
var out []int
for _, o := range orders {
if ok(o) {
out = append(out, o)
}
}
return out
}
func paid(cents int) bool { return cents > 0 }
func main() {
orders := []int{0, 450, 120, 0, 90}
fmt.Println(keep(orders, paid))
}Run:
go run keep.goOutput:
[450 120 90]
paid matches func(int) bool, so it is a valid argument. You can also pass a literal: keep(orders, func(c int) bool { return c >= 100 }).
Case 2: A named function type, HTTP-style
Save as route.go. An HTTP server is this pattern with extra types: a path in, a function that handles it. Keep the same shape at the desk — no port, no net/http yet.
// route.go
package main
import "fmt"
type Handler func(table int, note string)
func route(table int, note string, h Handler) {
h(table, note)
}
func kitchen(table int, note string) {
fmt.Printf("kitchen table %d: %s\n", table, note)
}
func bar(table int, note string) {
fmt.Printf("bar table %d: %s\n", table, note)
}
func main() {
route(4, "two coffees", bar)
route(11, "soup", kitchen)
route(7, "hold the onions", func(table int, note string) {
fmt.Printf("note on table %d: %s\n", table, note)
})
}Run:
go run route.goOutput:
bar table 4: two coffees
kitchen table 11: soup
note on table 7: hold the onions
type Handler func(...) is a name for a function type. It is not a class. kitchen and bar are ordinary functions that happen to match. The last call is a one-off literal — fine when it is three lines.
Case 3: A closure that keeps state
Save as counter.go. makeCounter returns a function. The local n lives as long as that function does.
// counter.go
package main
import "fmt"
func makeCounter(start int) func() int {
n := start
return func() int {
n++
return n
}
}
func main() {
next := makeCounter(40)
fmt.Println(next())
fmt.Println(next())
fmt.Println(next())
}Run:
go run counter.goOutput:
41
42
43
Two counters are two ns. makeCounter(40) and makeCounter(40) again do not share memory. That is the usual way to hide a little state without a struct — until the state grows, at which point you want a struct.
Case 4: Loop variables, modern Go
Save as greet_loop.go. Each closure prints the name from its iteration. In Go 1.21 and earlier this program would print Chen three times. It does not, on Go 1.27.
// greet_loop.go
package main
import "fmt"
func main() {
names := []string{"Amina", "Bo", "Chen"}
var greet []func()
for _, name := range names {
greet = append(greet, func() {
fmt.Println("hello", name)
})
}
for _, g := range greet {
g()
}
}Run:
go run greet_loop.goOutput:
hello Amina
hello Bo
hello Chen
If you must target an old compiler, the old workaround was name := name inside the loop. You do not need that here. You still need to be careful with variables declared outside the loop — next section.
The trap
A closure captures the variable, not a snapshot of its value. Assign after you build the function, and the function sees the new value.
Save as shift_greet.go:
// shift_greet.go
package main
import "fmt"
func main() {
shift := "Amina"
greet := func() {
fmt.Println("hello", shift)
}
greet()
shift = "Bo"
greet()
}Run:
go run shift_greet.goOutput:
hello Amina
hello Bo
That is correct Go and a surprise if you expected a copy. The fix is to pass the value in, so the closure’s parameter is its own:
// shift_greet_fix.go
package main
import "fmt"
func main() {
shift := "Amina"
greet := func(name string) {
fmt.Println("hello", name)
}
greet(shift)
shift = "Bo"
greet(shift)
}Run:
go run shift_greet_fix.goOutput:
hello Amina
hello Bo
Same output, clearer contract: greet takes a name. If you truly want a snapshot, copy into a new variable before the literal (name := shift) and close over name.
The boring rule
- Give function types a name when they appear more than once (
Handler,Predicate). Leave them inline when they appear once. - Prefer a named function (
paid,kitchen) over an anonymous blob that hides in a call. - Closures are for small captured state. When you have three captured variables, use a struct and a method.
- On Go 1.22+, loop vars are per-iteration. Still pass values into callbacks that outlive the function if the intent is “this value,” not “whatever that variable is later.”
nilfunction values panic when called. Check, or do not storenil.
Try this
- In
keep.go, pass a literal that keeps orders of at least 100 cents. Do not add a new named function. - In
route.go, add aHandlerthat printsvoid table Nand route table3with an empty note. - In
counter.go, make two counters frommakeCounter(0)and call each twice. Confirm they do not sharen. - In
shift_greet.go, snapshot withname := shiftbefore the literal and close overname. Changeshiftafter. What prints?