Functions, Closures and Defer

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18; every go.mod in the examples says go 1.27. Functions are the unit of reuse in Go, and the language gives them a small but distinctive toolbox: several results per call, first-class function values with closures, and three related statements — defer, panic and recover — that replace both finally blocks and exceptions. The page follows the order in which those features build on each other: declarations and results, variadics, function values and closures, how arguments are passed, the two idioms for optional parameters, then defer, panic and recover, and finally init functions. Pointers, which decide what "passed by value" means for a caller, are covered on Pointers and Value Semantics; the error values that functions return are covered on Error Handling.

Every example uses the shelf scenario (a small library catalogue). Examples were compiled with go vet and run with Go 1.27.2; the output shown is what they printed.

Function declarations

A function declaration binds an identifier to a function: the keyword func, a name, a signature (parameters and results) and a body. A function without results needs no return; a function with results must end in a terminating statement (see Control Flow).

func name(parameters) results {
    body
}

The rules worth knowing:

  • Parameters of consecutive same-typed names share a type: func clamp(v, lo, hi int) int.

  • Parameters and results are variables of the function’s block, so a parameter cannot be redeclared with := in the outermost block of the body (an inner block may shadow it).

  • Unused parameters are allowed (unlike unused local variables). A parameter you do not use can be named _ or left unnamed when every parameter in the list is unnamed.

  • A call may be passed a multi-valued call directly: if f returns (int, string) and g takes (int, string), g(f()) is legal, and is the only form where a call with several results appears as an argument.

  • There are no overloads, default values or named arguments. Each package-level name is unique. The two idioms for optional parameters are shown further down.

  • Functions are not nested declarations. A function declaration is only allowed at package level; inside a function you use a function literal (below) bound to a variable.

package main

import "fmt"

// Parameters of the same type can share one type; parameters may be unnamed or blank.
func clamp(v, lo, hi int) int { return min(max(v, lo), hi) }

func ignore(_ int, name string) string { return name }

// Results can be named and the function can still return explicit values.
func divmod(a, b int) (q, r int, err error) {
	if b == 0 {
		err = fmt.Errorf("divmod %d by zero", a)
		return // q and r are 0
	}
	return a / b, a % b, nil
}

func main() {
	fmt.Println(clamp(15, 0, 10), clamp(-3, 0, 10))
	fmt.Println(ignore(1, "shelf"))
	fmt.Println(divmod(17, 5))
	fmt.Println(divmod(1, 0))

	// A multi-valued call can be passed straight to another function
	// whose parameters match.
	fmt.Println(divmod(9, 2))
}
10 0
shelf
3 2 <nil>
0 0 divmod 1 by zero
4 1 <nil>

Multiple results

Returning several values is the idiom behind error handling: the last result is conventionally an error, and callers check it immediately. The ParseISBN function below returns the value and the error, and wraps a sentinel error with %w so callers can test it with errors.Is.

package main

import (
	"errors"
	"fmt"
	"strings"
)

// ISBN is a validated 13-digit identifier.
type ISBN string

var ErrBadISBN = errors.New("invalid ISBN")

// ParseISBN returns the ISBN and an error: the usual Go shape for "can fail".
func ParseISBN(s string) (ISBN, error) {
	digits := strings.ReplaceAll(s, "-", "")
	if len(digits) != 13 {
		return "", fmt.Errorf("parse %q: %w", s, ErrBadISBN)
	}
	return ISBN(digits), nil
}

// minMax uses named results: they document the order and start as zero values.
func minMax(xs ...int) (lo, hi int, ok bool) {
	if len(xs) == 0 {
		return // naked return: allowed, but discouraged in anything longer than a few lines
	}
	lo, hi = xs[0], xs[0]
	for _, x := range xs[1:] {
		lo = min(lo, x)
		hi = max(hi, x)
	}
	return lo, hi, true
}

func main() {
	isbn, err := ParseISBN("978-0-13-419044-0")
	fmt.Println(isbn, err)
	_, err = ParseISBN("12")
	fmt.Println(err, errors.Is(err, ErrBadISBN))

	fmt.Println(minMax(3, 1, 4, 1, 5))
	fmt.Println(minMax())
}
9780134190440 <nil>
parse "12": invalid ISBN true
1 5 true
0 0 false

The (T, error) pair is a convention, not a language feature; the error type and the wrapping verbs are covered in Error Handling. Results are not a tuple type: you cannot store "the results of a call" in one variable, assign them to one variable, or pass them to a function that takes a single parameter.

The compiler reports each of these mistakes, shown here for one scratch file that tries all of them. The last line is the defer form; the others are the multi-value and function-type mismatches.

package main

import "fmt"

func two() (int, string) { return 1, "a" }

func one(n int) {}

func main() {
	x := two()
	one(two())
	fmt.Println(x)
	var f func(int) = func(s string) {}
	_ = f
	defer recover
}
# example.com/w5/ce/c4
./main.go:10:7: assignment mismatch: 1 variable but two returns 2 values
./main.go:11:6: too many arguments in call to one
	have (int, string)
	want (int)
./main.go:13:20: cannot use func(s string) {} (value of type func(s string)) as func(int) value in variable declaration
./main.go:15:8: expression in defer must be function call

Named results and naked returns

Result parameters can be named, as minMax above does with lo, hi int, ok bool. Named results are ordinary variables that start at the zero value of their type. They have two proper uses:

  1. Documentation: when a function returns two values of the same type, names say which is which (func divmod(a, b int) (q, r int, err error) reads better than (int, int, error)).

  2. Deferred modification: a deferred function can read and change named results after return has assigned them, which is how cleanup code reports errors (see below).

A naked return (a return with no operands in a function that has named results) returns the current values of the result variables. It is legal, short, and a recurring source of bugs in anything longer than a handful of lines: the reader has to trace the whole body to learn what comes back. The Go Code Review Comments wiki recommends naked returns only in short functions. A related compiler rule exists because of shadowing: if an inner block redeclares a result name with := and then does a naked return in that scope, the compiler refuses.

package main

import "fmt"

func parse(s string) (n int, err error) {
	if s != "" {
		n, err := fmt.Sscanf(s, "%d", &n) // shadows both results
		if err != nil {
			return
		}
		_ = n
	}
	return
}

func main() { fmt.Println(parse("1")) }
# example.com/w5/ce/c2
./main.go:9:4: result parameter n not in scope at return
	./main.go:7:3: inner declaration of var n int
./main.go:9:4: result parameter err not in scope at return
	./main.go:7:6: inner declaration of var err error

The recommendation used throughout the shelf code: name results when the names add information or when a deferred function needs them, and return the values explicitly otherwise.

Terminating statements and "missing return"

A function with results must end in a terminating statement: a return or goto, a call to the built-in panic, a block ending in a terminating statement, an if with both branches terminating, a for loop with no condition and no break, or a switch/select whose every case terminates and that has a default. The compiler does not do flow analysis beyond that list, so an if/else if chain without a final else is not terminating even when it is exhaustive in practice.

package main

func sign(n int) int {
	if n > 0 {
		return 1
	} else if n < 0 {
		return -1
	}
}

func main() { _ = sign(1) }
# example.com/w5/ce/c1
./main.go:9:1: missing return

Variadic functions

The final parameter of a function may be variadic, written ...T. Inside the function it has type []T. A caller may pass any number of values, or a slice followed by ..., in which case no new slice is created: the function receives the caller’s slice and shares its backing array.

package main

import "fmt"

func sum(label string, nums ...int) int {
	fmt.Printf("%s: nums=%v len=%d nil=%t\n", label, nums, len(nums), nums == nil)
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

// double modifies the slice it is given: with slice... that is the caller's array.
func double(nums ...int) {
	for i := range nums {
		nums[i] *= 2
	}
}

func main() {
	fmt.Println(sum("none"))
	fmt.Println(sum("three", 1, 2, 3))
	xs := []int{4, 5, 6}
	fmt.Println(sum("spread", xs...))

	double(xs...)
	fmt.Println("after double(xs...):", xs)

	double(1, 2, 3) // a fresh slice is built for the call
	var empty []int
	fmt.Println(sum("nil slice", empty...))

	// The empty interface case: []any is not []string.
	names := []string{"Dune", "Emma"}
	args := make([]any, len(names))
	for i, n := range names {
		args[i] = n
	}
	fmt.Println(args...)
}
none: nums=[] len=0 nil=true
0
three: nums=[1 2 3] len=3 nil=false
6
spread: nums=[4 5 6] len=3 nil=false
15
after double(xs...): [8 10 12]
nil slice: nums=[] len=0 nil=true
0
Dune Emma

Details that surprise newcomers:

  • With no arguments, the variadic parameter is nil (not an empty non-nil slice).

  • Passing xs... lets the callee modify the caller’s elements, as double did above; passing individual values builds a fresh slice.

  • There is no automatic conversion between slice types: a []string cannot be passed as ...any, and the loop that builds a []any is the standard workaround.

  • Only the final parameter can be variadic, a call cannot mix individual values and xs..., and the variadic parameter’s type includes the ... in the function type (func(...int) and func([]int) are different types).

package main

import "fmt"

func sum(nums ...int) int { return len(nums) }

func main() {
	xs := []int{1, 2}
	fmt.Println(sum(xs))
	fmt.Println(sum(0, xs...))
	names := []string{"a"}
	fmt.Println(names...)
}
# example.com/w5/ce/c3
./main.go:9:18: cannot use xs (variable of type []int) as int value in argument to sum
./main.go:10:21: too many arguments in call to sum
	have (number, []int...)
	want (...int)
./main.go:12:14: cannot use names (variable of type []string) as []any value in argument to fmt.Println

Function types, values and literals

A function type denotes the set of functions with the same parameter and result types. Parameter and result names are not part of the type, but the number and types of parameters and results, and whether the last parameter is variadic, all are. Functions are first-class values: they can be assigned to variables, stored in structs and maps, passed as arguments and returned from other functions.

A type declaration can name a function type and give it methods. That is how the standard library turns a plain function into an http.Handler (http.HandlerFunc) and how sort-style helpers carry behaviour.

package main

import "fmt"

func a(xs ...int) int      { return len(xs) }
func b(xs []int) int       { return len(xs) }
func c(x int) (int, error) { return x, nil }
func d(x int) int          { return x }

func main() {
	var f func(...int) int = a
	f = b
	var g func(int) int = c
	g = d
	fmt.Println(f, g)
}
# example.com/w5/ce/c6
./main.go:12:6: cannot use b (value of type func(xs []int) int) as func(...int) int value in assignment
./main.go:13:24: cannot use c (value of type func(x int) (int, error)) as func(int) int value in variable declaration

A function literal (an anonymous function) has the same syntax as a declaration without a name. It can be called immediately, assigned to a variable, or passed along.

package main

import (
	"fmt"
	"strings"
)

// A function type names a signature; results and variadic-ness are part of it.
type Predicate func(title string) bool

// Methods can be declared on function types; this is how http.HandlerFunc works.
func (p Predicate) Not() Predicate { return func(t string) bool { return !p(t) } }

func filter(titles []string, keep Predicate) []string {
	var out []string
	for _, t := range titles {
		if keep(t) {
			out = append(out, t)
		}
	}
	return out
}

func main() {
	titles := []string{"Dune", "Dune Messiah", "Emma", "Ulysses"}

	startsWithD := func(t string) bool { return strings.HasPrefix(t, "D") }
	fmt.Println(filter(titles, startsWithD))
	fmt.Println(filter(titles, Predicate(startsWithD).Not()))

	// Function values are comparable only to nil.
	var f Predicate
	fmt.Println("nil func:", f == nil)

	// An immediately invoked function literal scopes temporaries.
	total := func() int {
		n := 0
		for i := range 5 {
			n += i
		}
		return n
	}()
	fmt.Println("total:", total)

	// A recursive literal must be declared first, so it can refer to itself.
	var fib func(int) int
	fib = func(n int) int {
		if n < 2 {
			return n
		}
		return fib(n-1) + fib(n-2)
	}
	fmt.Println("fib(10):", fib(10))

	// Calling a nil function value panics.
	defer func() { fmt.Println("recovered:", recover()) }()
	f("x")
}
[Dune Dune Messiah]
[Emma Ulysses]
nil func: true
total: 10
fib(10): 55
recovered: runtime error: invalid memory address or nil pointer dereference

Things to remember about function values:

  • The zero value is nil; calling it panics with a nil pointer dereference (shown above).

  • Function values are comparable only to nil. You cannot use them as map keys or compare two of them with ==.

  • A recursive literal needs the variable declared first (var fib func(int) int), because the name is not in scope inside its own initializer.

  • Method values (obj.Method) and method expressions (T.Method) also produce function values; they are covered in Methods and Receivers.

  • Function literals that capture variables are closures; the next section explains what is captured and when.

Closures

A function literal can refer to variables of the enclosing function. Those variables are shared by reference between the enclosing function and the closure for as long as either is alive. The variable’s lifetime is extended to match, so counter below can return a closure that keeps incrementing a local that would otherwise have died with the call (the compiler moves such a variable to the heap; see Pointers and Value Semantics).

package main

import "fmt"

// counter returns a closure that captures n by reference.
func counter() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

func main() {
	a, b := counter(), counter()
	fmt.Println(a(), a(), a(), b()) // a and b each have their own n

	// Each closure shares the variable, not a copy of its value at creation.
	x := 1
	show := func() { fmt.Println("x is", x) }
	x = 2
	show()

	// Since Go 1.22 (modules with go 1.22 or later), each iteration has its own variable.
	var fs []func() int
	for i := 0; i < 3; i++ {
		fs = append(fs, func() int { return i })
	}
	for _, f := range fs {
		fmt.Print(f(), " ")
	}
	fmt.Println()

	// A variable declared outside the loop is shared by every closure.
	var shared int
	var gs []func() int
	for shared = 0; shared < 3; shared++ {
		gs = append(gs, func() int { return shared })
	}
	for _, g := range gs {
		fmt.Print(g(), " ")
	}
	fmt.Println()
}
1 2 3 1
x is 2
0 1 2
3 3 3

The four lines show the whole model:

  1. Each call to counter creates a fresh n, so a and b are independent.

  2. A closure captures the variable, not its value: after x = 2, show prints 2.

  3. Since Go 1.22, a for loop’s variables are per-iteration, so each closure appended in the three-clause loop captures its own i and prints 0 1 2.

  4. A variable declared outside the loop (shared) is a single variable no matter what the loop does, so every closure sees its final value 3.

Loop variables and the Go 1.22 change

Before Go 1.22 the loop variable was created once per loop, which made "every goroutine prints the last value" one of the best-known Go pitfalls and made the v := v copy at the top of the loop body a standard idiom. Go 1.22 changed the semantics: each iteration has its own copy, and the variable for the next iteration is initialised from the previous one just before the post statement runs. The change is tied to the go line of the module’s go.mod: a module that declares go 1.22 or later gets the new behaviour, and older modules keep the old one, so upgrading the go line is what turns it on. Because the new semantics apply to every go 1.22+ module, copies of the loop variable are no longer needed in new code (the go fix modernizers remove them; see Formatting, vet, fix and Static Analysis). The goroutine side of the story is in Goroutines and the Scheduler.

What is captured, in practice

Situation What the closure sees

Variable declared in the enclosing function

The variable itself: reads and writes by either side are visible to both.

Per-iteration loop variable (go 1.22+)

The variable of that iteration only.

Variable declared outside the loop

One shared variable; all closures see its latest value.

Function parameter of the enclosing function

The parameter variable, which is a copy of the argument (call by value) — not the caller’s variable.

Method value obj.Method

The receiver, evaluated and (for a value receiver) copied when the method value is created.

Compiler note: Go 1.27 changed how the compiler names function literals so that the name no longer depends on inlining, and identical literals may share code in the binary. Names such as main.main.func1 still appear in stack traces, but tests that depend on the exact name of a function literal may need updating, and the release notes recommend not depending on those names at all.

Higher-order functions and generics

A higher-order function takes or returns functions. Combined with generics (Go 1.18), Map, Filter and Reduce become ordinary library code, and the standard library already supplies the most common ones in slices and maps (slices.SortFunc, slices.IndexFunc, slices.ContainsFunc, maps.Keys, …​). A function that builds a comparison function (byField below) is the usual way to adapt slices.SortFunc to a struct field.

package main

import (
	"cmp"
	"fmt"
	"slices"
	"strings"
)

type Book struct {
	Title string
	Year  int
}

// Map, Filter and Reduce are ordinary generic functions; the standard library
// offers slices.SortFunc, slices.IndexFunc and friends for the common cases.
func Map[T, U any](xs []T, f func(T) U) []U {
	out := make([]U, 0, len(xs))
	for _, x := range xs {
		out = append(out, f(x))
	}
	return out
}

func Reduce[T, A any](xs []T, init A, f func(A, T) A) A {
	acc := init
	for _, x := range xs {
		acc = f(acc, x)
	}
	return acc
}

// byField returns a comparison function: a function that builds a function.
func byField[T any, K cmp.Ordered](key func(T) K) func(a, b T) int {
	return func(a, b T) int { return cmp.Compare(key(a), key(b)) }
}

func main() {
	books := []Book{{"Ulysses", 1922}, {"Dune", 1965}, {"Emma", 1815}}

	slices.SortFunc(books, byField(func(b Book) int { return b.Year }))
	fmt.Println(books)

	titles := Map(books, func(b Book) string { return strings.ToUpper(b.Title) })
	fmt.Println(titles)

	oldest := Reduce(books, books[0], func(acc, b Book) Book {
		if b.Year < acc.Year {
			return b
		}
		return acc
	})
	fmt.Println(oldest)

	i := slices.IndexFunc(books, func(b Book) bool { return b.Title == "Dune" })
	fmt.Println("Dune at", i)
}
[{Emma 1815} {Ulysses 1922} {Dune 1965}]
[EMMA ULYSSES DUNE]
{Emma 1815}
Dune at 2

Generics have their own page, Generics, and range-over-function iterators (iter.Seq) are the modern alternative to returning slices from a pipeline; see Iterators and Range-over-Func.

Generic methods and wider inference (Go 1.27)

Go 1.27 lifted two limits that used to push generic code into package-level functions. A method declaration may declare its own type parameters, so (s *Shelf) Each[T any](...) is legal (interface methods still cannot have type parameters, and a generic method cannot implement an interface method). Type inference was also generalised: a generic function can be assigned to a variable (or converted to a function type) and its type arguments are inferred from the target type, as itoa does below.

package main

import (
	"fmt"
	"strconv"
)

type Shelf struct{ books []string }

// Go 1.27: a method may declare its own type parameters.
func (s *Shelf) Each[T any](f func(title string) T) []T {
	out := make([]T, 0, len(s.books))
	for _, b := range s.books {
		out = append(out, f(b))
	}
	return out
}

func Convert[T, U any](x T, f func(T) U) U { return f(x) }

func main() {
	s := &Shelf{books: []string{"Dune", "Emma"}}
	fmt.Println(s.Each(func(t string) int { return len(t) }))

	// Go 1.27: inference also works when a generic function is assigned to a typed variable.
	var itoa func(int, func(int) string) string = Convert
	fmt.Println(itoa(7, strconv.Itoa))
}
[4 4]
7

A module that still says go 1.25 in go.mod cannot use the feature; the compiler states the minimum version.

# example.com/old
./main.go:11:22: generic method requires go1.27 or later (-lang was set to go1.25; check go.mod)

Call by value

Every argument in Go is passed by value: the callee receives a copy of the argument and cannot change the caller’s variable. What differs is what the copy contains. Copying a pointer, slice header, map reference or channel copies a small handle, so changes made through the handle are visible to the caller, while changes made to the handle itself (re-slicing, reassigning) are not.

package main

import "fmt"

type Book struct {
	Title string
	Tags  []string
}

func renameValue(b Book)    { b.Title = "changed" }
func renamePointer(b *Book) { b.Title = "changed" }

// Appending inside the callee is invisible to the caller (the header was copied)...
func appendTag(tags []string) { tags = append(tags, "new") }

// ...but writing through the shared backing array is visible.
func setFirst(tags []string) { tags[0] = "first" }

// Maps and channels are references to runtime structures.
func addEntry(m map[string]int) { m["added"] = 1 }

func reassign(m map[string]int) { m = map[string]int{"other": 2} }

func main() {
	b := Book{Title: "Dune", Tags: []string{"scifi"}}
	renameValue(b)
	fmt.Println("after renameValue:", b.Title)
	renamePointer(&b)
	fmt.Println("after renamePointer:", b.Title)

	appendTag(b.Tags)
	fmt.Println("after appendTag:", b.Tags)
	setFirst(b.Tags)
	fmt.Println("after setFirst:", b.Tags)

	m := map[string]int{}
	addEntry(m)
	reassign(m)
	fmt.Println("map:", m)

	ch := make(chan int, 1)
	func(c chan int) { c <- 42 }(ch)
	fmt.Println("channel got:", <-ch)
}
after renameValue: Dune
after renamePointer: changed
after appendTag: [scifi]
after setFirst: [first]
map: map[added:1]
channel got: 42
Argument type What is copied What the callee can change in the caller

int, string, bool, arrays, structs

The whole value (a string copies only its two-word header; an array copies every element)

Nothing

pointer

The address

The pointed-to variable (not the caller’s pointer)

slice

The three-word header (pointer, length, capacity)

Existing elements of the shared array; not the caller’s length or capacity (an append result must be returned)

map

The reference to the runtime map

The map’s contents; reassigning the parameter does not affect the caller

channel

The reference to the channel

Anything communicated through it

function value, interface

The function reference, or the (type, data) pair of an interface

What the closure or the interface’s dynamic value allows

The practical consequences, which the sections on pointers and slices elaborate:

  • To let a callee change a struct, pass *T; to let it only read, pass T for small structs or *T for large ones.

  • A function that grows a slice must return it (xs = append(xs, v)), because append may reallocate and the caller’s header is a copy.

  • Arrays are values: func f(a [1000]int) copies a thousand ints on every call. Pass *[1000]int or a slice instead.

  • The same rule applies to the receiver of a method: a value receiver is a copy, a pointer receiver is shared.

Optional and named parameters

Go has neither default values nor named arguments, and it deliberately has no overloading. Two idioms fill the gap.

An options struct. When the zero value of each field is a sensible default, accept a struct and let callers fill only the fields they care about; field names give the "named parameter" syntax for free. Use pointers or a separate Set flag only for fields where zero is a meaningful value (see the zero-value discussion on Pointers and Value Semantics).

Functional options. For constructors with many optional settings, or whose options may grow over time, accept a variadic list of func(*T) values. Existing callers keep compiling when a new option appears, options can validate or combine, and the config fields stay unexported.

package main

import (
	"fmt"
	"time"
)

// Go has no default or named parameters. Two common substitutes follow.

// 1. An options struct: zero values mean "use the default".
type SearchOptions struct {
	Query    string
	Limit    int
	FromYear int
}

func Search(opts SearchOptions) string {
	if opts.Limit == 0 {
		opts.Limit = 10
	}
	return fmt.Sprintf("search %q limit=%d from=%d", opts.Query, opts.Limit, opts.FromYear)
}

// 2. Functional options: variadic functions that mutate an unexported config.
type Server struct {
	addr    string
	timeout time.Duration
	logger  func(string)
}

type Option func(*Server)

func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithLogger(f func(string)) Option   { return func(s *Server) { s.logger = f } }

func NewServer(addr string, opts ...Option) *Server {
	s := &Server{addr: addr, timeout: 30 * time.Second, logger: func(string) {}}
	for _, opt := range opts {
		opt(s)
	}
	return s
}

func main() {
	fmt.Println(Search(SearchOptions{Query: "dune"}))
	fmt.Println(Search(SearchOptions{Query: "dune", Limit: 3, FromYear: 1960}))

	s := NewServer(":8080", WithTimeout(5*time.Second), WithLogger(func(m string) { fmt.Println("log:", m) }))
	s.logger("listening on " + s.addr)
	fmt.Println(s.timeout)
	fmt.Println(NewServer(":9090").timeout)
}
search "dune" limit=10 from=0
search "dune" limit=3 from=1960
log: listening on :8080
5s
30s

Use the plain struct when the set of options is small and stable, and functional options when the API is public and has to evolve without breaking callers. Do not use ...any as an "optional arguments" bag: it loses type checking.

defer

A defer statement schedules a function call to run when the surrounding function returns — whether it returns normally, through a return, or because of a panic. Deferred calls are the idiomatic way to guarantee cleanup: closing files, unlocking mutexes, ending spans, restoring state. The call is pushed onto a per-function stack and the calls run in last-in, first-out order.

package main

import "fmt"

func trace(name string) func() {
	fmt.Println("enter", name)
	return func() { fmt.Println("exit", name) }
}

func work() {
	defer trace("work")() // trace("work") runs NOW; the returned func runs at exit
	defer fmt.Println("deferred 1")
	defer fmt.Println("deferred 2")
	defer fmt.Println("deferred 3")
	fmt.Println("body")
}

func args() {
	x := 1
	defer fmt.Println("arguments evaluated at defer time, x =", x)
	defer func() { fmt.Println("closure reads x at exit, x =", x) }()
	x = 2
}

func main() {
	work()
	args()
}
enter work
body
deferred 3
deferred 2
deferred 1
exit work
closure reads x at exit, x = 2
arguments evaluated at defer time, x = 1

Three rules, all in the specification:

  1. Arguments, and the function value and receiver, are evaluated when the defer statement executes, not when the call runs. That is why defer trace("work")() prints "enter" immediately and the returned closure prints "exit" last.

  2. Deferred functions run in LIFO order after the surrounding function’s result parameters have been set.

  3. Deferred function values may be closures, and a closure reads the variables as they are when it runs.

package main

import "fmt"

type Counter struct{ n int }

func (c Counter) ShowValue()    { fmt.Println("value receiver sees", c.n) }
func (c *Counter) ShowPointer() { fmt.Println("pointer receiver sees", c.n) }

func main() {
	c := Counter{n: 1}
	defer c.ShowValue()   // receiver copied NOW: will print 1
	defer c.ShowPointer() // &c taken now, but c.n read at exit: will print 2
	c.n = 2

	// A nil function value is evaluated at defer time but only panics when it runs.
	var f func()
	defer func() { fmt.Println("recovered:", recover()) }()
	defer f()
	fmt.Println("body done")
}
body done
recovered: runtime error: invalid memory address or nil pointer dereference
pointer receiver sees 2
value receiver sees 1

The example shows the evaluation-time rule for receivers (a value receiver is copied at the defer; a pointer receiver is the address taken then, but the field is read later) and the less obvious rule for a nil function value: the defer f() itself succeeds, and the panic happens only when the deferred call executes at function exit.

Modifying named results

Because deferred functions run after return has stored the result values, they can change named results. With unnamed results they cannot, which is why the third function below still returns 1.

package main

import (
	"errors"
	"fmt"
)

type resource struct{ closeErr error }

func (r *resource) Close() error { return r.closeErr }

// A deferred closure can read and change named results after return has set them.
func doubleOnReturn() (n int) {
	defer func() { n *= 2 }()
	return 21 // sets n = 21, then the deferred function runs
}

// Typical use: do not lose a Close error on a writer.
func write(r *resource, writeErr error) (err error) {
	defer func() {
		err = errors.Join(err, r.Close())
	}()
	return writeErr
}

// With unnamed results the deferred function cannot change what is returned.
func unnamed() int {
	n := 1
	defer func() { n = 100 }()
	return n
}

func main() {
	fmt.Println(doubleOnReturn())
	fmt.Println(unnamed())
	fmt.Println(write(&resource{}, nil))
	fmt.Println(write(&resource{closeErr: errors.New("close failed")}, nil))
	fmt.Println(write(&resource{closeErr: errors.New("close failed")}, errors.New("write failed")))
}
42
1
<nil>
close failed
write failed
close failed

This is the mechanism behind two common idioms: annotating an error on the way out (err = fmt.Errorf("load %s: %w", path, err) in a deferred closure) and combining an operation’s error with a cleanup error using errors.Join (added in Go 1.20), as write does above.

Check the Close error on files you write

Closing a file opened for reading can usually ignore the error. Closing a file opened for writing is different: on many filesystems data is only guaranteed to be accepted when Close succeeds, so ignoring its error can silently lose the tail of the file. The pattern that keeps the first error and still closes the file:

package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
)

// export writes lines to path. The Close error on a file opened for writing
// can be the only sign of a failed flush, so it is propagated.
func export(path string, lines []string) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); err == nil {
			err = cerr
		}
	}()

	w := bufio.NewWriter(f)
	for _, l := range lines {
		if _, err := fmt.Fprintln(w, l); err != nil {
			return err
		}
	}
	return w.Flush()
}

func main() {
	dir, _ := os.MkdirTemp("", "shelf")
	defer os.RemoveAll(dir)

	p := filepath.Join(dir, "books.txt")
	fmt.Println("export:", export(p, []string{"Dune", "Emma"}))
	data, _ := os.ReadFile(p)
	fmt.Printf("%q\n", data)

	// A path in a missing directory fails at Create.
	fmt.Println("missing dir:", export(filepath.Join(dir, "no", "such", "x.txt"), nil) != nil)
}
export: <nil>
"Dune\nEmma\n"
missing dir: true

Source: os: File.Close

Note the flush: bufio.Writer holds data in memory until Flush, and a failed Flush must be returned before the deferred Close runs.

defer in loops

A defer inside a loop does not run at the end of the iteration; it runs when the surrounding function returns, so every file opened in the loop stays open until then, and a long loop can exhaust file descriptors. Move the loop body into a function (or a closure that is called immediately) so each iteration gets its own scope.

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

// Wrong: nothing is closed until the function returns, so all files stay open.
func readAllBad(paths []string) {
	for _, p := range paths {
		f, err := os.Open(p)
		if err != nil {
			continue
		}
		defer f.Close() // runs when readAllBad returns, not at the end of each iteration
	}
	fmt.Println("bad: the deferred Close calls have not run yet")
}

// Right: one function call per iteration gives each file its own defer scope.
func firstByte(path string) (b byte, err error) {
	f, err := os.Open(path)
	if err != nil {
		return 0, err
	}
	defer f.Close() // read-only file: ignoring Close's error is acceptable

	buf := make([]byte, 1)
	if _, err := f.Read(buf); err != nil {
		return 0, err
	}
	return buf[0], nil
}

func main() {
	dir, err := os.MkdirTemp("", "shelf")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(dir)

	var paths []string
	for i, content := range []string{"alpha", "beta", "gamma"} {
		p := filepath.Join(dir, fmt.Sprintf("f%d.txt", i))
		if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
			panic(err)
		}
		paths = append(paths, p)
	}
	readAllBad(paths)
	for _, p := range paths {
		b, err := firstByte(p)
		fmt.Printf("%c %v\n", b, err)
	}
}
bad: the deferred Close calls have not run yet
a <nil>
b <nil>
g <nil>

Common defer mistakes

  • Evaluating too early: defer log.Println("elapsed:", time.Since(start)) evaluates time.Since(start) at the defer statement, so it prints (almost) zero. go vet reports it with the defers analyzer. Wrap it in a closure to measure at exit.

  • Deferring a call that has no effect on exit, for example defer f with the parentheses missing, is a compile error.

  • Deferring in a hot loop: since Go 1.14 most defers are almost free (open-coded defers), but a defer inside a loop cannot be handled that way and costs more.

  • Relying on defer with os.Exit or log.Fatal: both end the process immediately and run no deferred calls.

package main

import (
	"log"
	"time"
)

func main() {
	start := time.Now()
	defer log.Println("elapsed:", time.Since(start))
	time.Sleep(10 * time.Millisecond)
}
v1/main.go:10:32: call to time.Since is not deferred

Source: cmd/vet

package main

func f() int {
	defer func() int { return 1 }()
	return 0
}

func main() {
	defer f
	go func() {}
	f() = 1
}
# example.com/w5/ce/c5
./main.go:9:8: expression in defer must be function call
./main.go:10:5: expression in go must be function call
./main.go:11:2: cannot assign to f() (neither addressable nor a map index expression)

Go 1.14 made defer cheap: "most uses of defer to incur almost zero overhead", the release notes say, so the old advice to avoid defer in performance-critical code no longer applies outside loops.

panic and recover

panic is a built-in function that stops the normal flow of the current goroutine. recover is a built-in function that regains control of a panicking goroutine. Together they are Go’s mechanism for exceptional conditions — bugs, violated invariants, unrecoverable states — not for ordinary errors, which are values returned from functions.

What happens when a function panics

When panic(v) is called (or a run-time error such as an out-of-range index occurs):

  1. The current function stops. Its deferred calls run, in LIFO order.

  2. The panic unwinds to the caller, whose deferred calls run, and so on up the goroutine’s stack.

  3. If a deferred function calls recover() directly, the panic stops: recover returns the value passed to panic, the deferred function finishes, and the function that deferred it returns to its caller as if it had returned normally (with whatever its named results then hold).

  4. If the unwinding reaches the top of the goroutine without a recover, the program prints the panic value and a stack trace and exits with status 2. This applies to the whole program, not only the panicking goroutine.

sequenceDiagram participant M as main participant L1 as level1 (defers recover) participant L2 as level2 participant L3 as level3 M->>L1: call level1() L1->>L1: defer recover closure L1->>L2: call level2() L2->>L2: defer step("level2 deferred") L2->>L3: call level3() L3->>L3: defer step("level3 deferred") L3->>L3: panic("disk on fire") Note over L3: normal flow stops, unwinding begins L3->>L3: run deferred: level3 deferred L3--xL2: panic continues in the caller L2->>L2: run deferred: level2 deferred L2--xL1: panic continues in the caller L1->>L1: run deferred: recover() returns "disk on fire" Note over L1: panic stopped, err is set through the named result L1-->>M: return err (normal return)

The diagram follows the program below, which prints step ... lines in the order the deferred calls run.

package main

import (
	"errors"
	"fmt"
	"runtime"
)

func step(name string) { fmt.Println("step", name) }

func level3() {
	defer step("level3 deferred")
	panic("disk on fire")
}

func level2() {
	defer step("level2 deferred")
	level3()
	fmt.Println("level2 never gets here")
}

func level1() (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r)
		}
	}()
	defer step("level1 deferred")
	level2()
	return nil
}

func main() {
	fmt.Println(level1())

	// recover returns nil when the goroutine is not panicking.
	fmt.Println("recover outside panic:", recover())

	// A runtime error is a panic with a value implementing runtime.Error.
	func() {
		defer func() {
			r := recover()
			var re runtime.Error
			err, _ := r.(error)
			fmt.Println("is runtime.Error:", errors.As(err, &re), "|", r)
		}()
		var xs []int
		_ = xs[3]
	}()
}
step level3 deferred
step level2 deferred
step level1 deferred
recovered: disk on fire
recover outside panic: <nil>
is runtime.Error: true | runtime error: index out of range [3] with length 0

The third part of the output shows that a run-time error is an ordinary panic whose value implements the runtime.Error interface, and that recover() outside a panic returns nil.

The rules of recover

recover has three restrictions that cause most of the confusion:

  1. It only has an effect when called directly by a deferred function. A helper that calls recover() and is called from the deferred function sees nil.

  2. It only returns a non-nil value while the goroutine is panicking and the deferred function was run by the panic’s unwinding.

  3. It recovers only the panic of its own goroutine; a panic in another goroutine cannot be caught from here.

package main

import "fmt"

func helper() any { return recover() }

// recover only stops a panic when it is called DIRECTLY by a deferred function.
func viaClosure() {
	defer func() {
		// helper is an ordinary call from the deferred function, one level too deep.
		fmt.Println("recover inside a helper:", helper())
	}()
	panic("B")
}

func viaDeferredHelper() {
	defer func() { fmt.Println("second recover:", recover()) }()
	defer helper() // here helper itself is the deferred function, so its recover works
	panic("C")
}

func main() {
	func() {
		defer func() { fmt.Println("outer recover caught:", recover()) }()
		viaClosure()
	}()
	viaDeferredHelper()
}
recover inside a helper: <nil>
outer recover caught: B
second recover: <nil>

In viaDeferredHelper, helper itself is the deferred function, so its recover is a direct call and stops the panic (its return value is discarded, which is why the next deferred call sees nil). In viaClosure the helper is one call too deep, so the panic goes on to the outer recover.

panic(nil) and PanicNilError (Go 1.21)

Before Go 1.21, panic(nil) made recover() return nil, which was indistinguishable from "no panic" and let code accidentally swallow a panic. Since Go 1.21, panic(nil) panics with a *runtime.PanicNilError, so recover always returns a non-nil value after a panic.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	defer func() {
		r := recover()
		fmt.Printf("%T: %v\n", r, r)
		if pe, ok := r.(*runtime.PanicNilError); ok {
			fmt.Println("PanicNilError:", pe.Error())
		}
	}()
	panic(nil) // since Go 1.21 this panics with *runtime.PanicNilError
}
*runtime.PanicNilError: runtime error: panic called with nil argument
PanicNilError: runtime error: panic called with nil argument

The old behaviour can be restored per module with GODEBUG=panicnil=1 (in go.mod as a godebug directive, or //go:debug panicnil=1); a module whose go line is older than 1.21 gets it by default. New code should not need it.

The panics the runtime raises

The built-in operations that can fail at run time panic with a runtime.Error. The messages below were produced by recovering each one.

package main

import (
	"fmt"
	"os"
	"runtime"
)

type Shape interface{ Area() float64 }

func try(name string, f func()) {
	defer func() {
		r := recover()
		_, isRuntime := r.(runtime.Error)
		fmt.Printf("%-21s %-3t %v\n", name+":", isRuntime, r)
	}()
	f()
}

func main() {
	var (
		xs   []int
		m    map[string]int
		p    *os.File
		zero int
		ch       = make(chan int)
		sh   any = "text"
		n        = -1
	)
	try("nil dereference", func() { _ = p.Name() })
	try("index out of range", func() { _ = xs[5] })
	try("slice bounds", func() { _ = xs[1:3] })
	try("nil map write", func() { m["k"] = 1 })
	try("integer divide", func() { _ = 10 / zero })
	try("type assertion", func() { _ = sh.(int) })
	try("interface assertion", func() { _ = sh.(Shape) })
	try("close closed chan", func() { close(ch); close(ch) })
	try("send on closed", func() { ch <- 1 })
	try("close nil chan", func() { var c chan int; close(c) })
	try("negative make", func() { _ = make([]int, n) })
	try("slice to array", func() { _ = [4]int(xs) })
	try("custom error", func() { panic(fmt.Errorf("shelf %q is full", "A1")) })
}
nil dereference:      true runtime error: invalid memory address or nil pointer dereference
index out of range:   true runtime error: index out of range [5] with length 0
slice bounds:         true runtime error: slice bounds out of range [:3] with capacity 0
nil map write:        true assignment to entry in nil map
integer divide:       true runtime error: integer divide by zero
type assertion:       true interface conversion: interface {} is string, not int
interface assertion:  true interface conversion: string is not main.Shape: missing method Area
close closed chan:    true close of closed channel
send on closed:       true send on closed channel
close nil chan:       true close of nil channel
negative make:        true runtime error: makeslice: len out of range
slice to array:       true runtime error: cannot convert slice with length 0 to array or pointer to array with length 4
custom error:         false shelf "A1" is full

Several of those messages do not start with runtime error: (the nil-map write, the channel misuse and the failed type assertions), but they are still runtime.Error values, as the true/false column (is it a runtime.Error) shows; the error created with fmt.Errorf is the only plain one. Stack overflow, running out of memory, a concurrent map read/write detected by the runtime, and "all goroutines are asleep - deadlock!" are fatal errors: they terminate the program and cannot be recovered.

When to use panic

The convention in the standard library and in Effective Go: use error for anything that can fail in normal operation (bad input, missing file, a network timeout), and panic only when

  • a programming error has been detected that the function cannot continue from (an impossible switch case, a violated invariant, an index that was supposed to be validated);

  • initialisation of a package-level value fails and the program cannot start (regexp.MustCompile, template.Must, the Must... helper convention);

  • a package uses panic internally to unwind deep recursion (parsers, encoders) and converts it to an error at its public boundary.

Recovering at a library boundary

The third use is the one that needs recover. The package panics internally with a private type, and its exported functions recover that type and return an error. A recover that sees any other value must re-panic, so real bugs (nil dereferences, index errors) are not hidden.

package main

import (
	"errors"
	"fmt"
	"strconv"
)

// Parse is a library entry point: internally it may panic, but it never lets a panic
// cross the package boundary.
type parseError struct{ msg string }

func (e parseError) Error() string { return e.msg }

func mustDigit(r rune) int {
	if r < '0' || r > '9' {
		panic(parseError{"not a digit: " + strconv.QuoteRune(r)})
	}
	return int(r - '0')
}

func Parse(s string) (n int, err error) {
	defer func() {
		switch r := recover().(type) {
		case nil:
		case parseError:
			err = r
		default:
			panic(r) // not ours: re-panic so real bugs stay visible
		}
	}()
	if s == "bug" {
		var m map[string]int
		m["x"] = 1 // a genuine bug, not a parseError
	}
	for _, r := range s {
		n = n*10 + mustDigit(r)
	}
	return n, nil
}

func main() {
	fmt.Println(Parse("1234"))
	n, err := Parse("12x4")
	var pe parseError
	fmt.Println(n, err, errors.As(err, &pe))

	defer func() { fmt.Println("main recovered the re-panic:", recover()) }()
	Parse("bug")
}
1234 <nil>
12 not a digit: 'x' true
main recovered the re-panic: assignment to entry in nil map

A re-panic keeps the original in the trace: the unrecovered output reads panic: original [recovered] followed by the new panic, so the chain of causes remains visible.

cleaning up after: original
panic: original [recovered]
	panic: re-panic: original

Recovering at a goroutine boundary

A panic that reaches the top of any goroutine ends the whole process, and cannot be recovered by the code that started the goroutine. A server that must survive a bug in one request handler (the net/http server does this for you, logging the panic and closing the connection) or a worker pool therefore puts a deferred recover at the start of each goroutine. sync.WaitGroup.Go (Go 1.25) starts a goroutine and tracks it in one call; the recover still belongs inside the function it runs.

package main

import (
	"fmt"
	"sync"
)

// A panic in a goroutine cannot be recovered by the goroutine that started it.
// Each goroutine that might panic needs its own deferred recover.
func safeGo(wg *sync.WaitGroup, id int, f func()) {
	wg.Go(func() {
		defer func() {
			if r := recover(); r != nil {
				fmt.Printf("worker %d recovered: %v\n", id, r)
			}
		}()
		f()
	})
}

func main() {
	var wg sync.WaitGroup
	safeGo(&wg, 1, func() { panic("boom") })
	wg.Wait()
	fmt.Println("main continues")
}
worker 1 recovered: boom
main continues

Recovering is not the same as handling: a recovered panic often indicates corrupted state, so log it with its stack (debug.Stack() or runtime/debug.PrintStack) and decide whether to continue.

An unrecovered panic

If nothing recovers, the runtime runs the deferred calls of the panicking goroutine, prints panic: ` followed by the value, then a goroutine stack trace, and exits with status 2. For an `error value it prints the result of Error(); for a runtime error it adds a [signal ...] line with a hardware address (the stack and addresses vary between runs, so only the first lines are shown).

deferred in main
panic: fatal: file does not exist

With a nil pointer dereference the same program prints:

deferred in main
panic: runtime error: invalid memory address or nil pointer dereference

(The next line is the [signal SIGSEGV ...] report followed by the stack trace.)

runtime.Goexit and os.Exit

runtime.Goexit terminates the calling goroutine after running all of its deferred calls; other goroutines are unaffected, and recover returns nil during a Goexit. If called from the main goroutine, the program keeps running other goroutines and ends with a "no goroutines" fatal error when they finish. os.Exit terminates the whole process immediately with the given status, and runs no deferred calls, so the third deferred call below never prints. The program exits with status 3.

package main

import (
	"fmt"
	"os"
	"runtime"
	"sync"
)

func main() {
	defer fmt.Println("main deferred runs")
	var wg sync.WaitGroup
	wg.Go(func() {
		defer fmt.Println("goroutine deferred runs after Goexit")
		runtime.Goexit() // terminates only this goroutine, running its defers
		fmt.Println("unreachable")
	})
	wg.Wait()
	defer fmt.Println("never printed: os.Exit does not run defers")
	os.Exit(3)
}
goroutine deferred runs after Goexit

Source: runtime: Goexit

testing.T.FailNow and T.Fatal use Goexit to stop a test goroutine while still running its deferred cleanups.

init functions

A function named init with no parameters and no results may be declared any number of times in a package, even several in the same file. The compiler runs them after all package-level variables are initialised and after every imported package has been initialised, in the order the files are presented to the compiler (the go command sorts them by file name). init cannot be called or referenced. Because init runs implicitly, it is hard to test and easy to overuse: prefer explicit constructors, and keep init for things such as registering a driver or a codec.

package main

import "fmt"

var order = initOrder()

func initOrder() string {
	fmt.Println("package-level variable initialised")
	return "ready"
}

func init() { fmt.Println("init 1:", order) }
func init() { fmt.Println("init 2") }

func main() { fmt.Println("main") }
package-level variable initialised
init 1: ready
init 2
main

The initialization order across packages, the blank import (import _ "...") used to register database/sql drivers, and the practice of avoiding init are covered on Packages, Imports and Initialization.

Compile-time checks and tools

Tool or message What it catches

missing return

A function with results whose body does not end in a terminating statement.

result parameter n not in scope at return

A naked return after an inner block shadowed a named result.

expression in defer must be function call

defer f without parentheses, or go/defer applied to a non-call.

too many arguments in call to f / cannot use ... as ... value in argument

Wrong number or type of arguments, including xs instead of xs....

go vet: call to time.Since is not deferred

The defers analyzer: a call argument evaluated at defer time that was meant to run at exit.

go vet: ... passes lock by value

The copylocks analyzer: a value receiver or parameter that copies a sync.Mutex (see Pointers and Value Semantics).

go vet: lostcancel

A context.WithCancel cancel function that is not called on all paths; deferring it is the standard fix.

gofmt, go fix

Formatting, and modernizers that remove the pre-1.22 v := v copies.

Source: cmd/vet

Where the books are outdated

Both books predate several changes that touch the material on this page.

  • Bodner, chapter 5 (functions) still matches the current language for call by value, variadic parameters, named results, defer and the advice to avoid naked returns. The loop-variable pitfall it touches on, and the copy idiom taught in the for and goroutine chapters (4 and 10), are obsolete in modules declaring go 1.22 or later; see the loop-variable section above.

  • Vivien, chapter 5 describes a function type as determined by its parameters; the results and the variadic parameter are part of the type as well (see the compile error above). Its panic/recover discussion predates the *runtime.PanicNilError behaviour of Go 1.21, so recover() is now never nil after a panic. Several of its examples treat an ordinary failure (a file that already exists) as a panic and do not check the Close error on files opened for writing; this page shows the error-returning forms instead. Its goroutine examples (chapter 9) rely on the old shared loop variable; the per-iteration variables of Go 1.22 make the direct form correct.

  • Bodner, chapter 8 recommends recovering at library boundaries and converting the panic to an error; that advice remains sound and is what the library-boundary section above shows, with errors.As and the errors.Join of Go 1.20 available for the error side.

  • Neither book covers the generic Map/Filter/Reduce helpers that are plain library code since Go 1.18 (Bodner’s generics chapter predates the final design), the slices and maps packages (1.21), range-over-function iterators (1.23), sync.WaitGroup.Go (1.25) or generic methods (1.27).

Version notes

Version Change

Go 1.14

defer overhead reduced to almost zero in most cases; goroutines asynchronously preemptible.

Go 1.18

Generics, so higher-order helpers can be typed.

Go 1.20

errors.Join, and multiple %w in fmt.Errorf, for combining a primary and a cleanup error.

Go 1.21

panic(nil) becomes *runtime.PanicNilError (GODEBUG=panicnil); slices and maps packages; min/max builtins.

Go 1.22

Per-iteration loop variables (modules with go 1.22+); range over integers.

Go 1.23

Range-over-function iterators (iter.Seq).

Go 1.25

sync.WaitGroup.Go.

Go 1.26

new(expr) (see Pointers and Value Semantics).

Go 1.27

Generic methods; type inference when assigning a generic function to a function-typed variable; function-literal naming no longer depends on inlining.