Functions as Values

Updated

September 13, 2026

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.go

Output:

[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.go

Output:

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.go

Output:

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.go

Output:

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.go

Output:

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.go

Output:

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.”
  • nil function values panic when called. Check, or do not store nil.

Try this

  1. In keep.go, pass a literal that keeps orders of at least 100 cents. Do not add a new named function.
  2. In route.go, add a Handler that prints void table N and route table 3 with an empty note.
  3. In counter.go, make two counters from makeCounter(0) and call each twice. Confirm they do not share n.
  4. In shift_greet.go, snapshot with name := shift before the literal and close over name. Change shift after. What prints?