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
freturns(int, string)andgtakes(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:
-
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)). -
Deferred modification: a deferred function can read and change named results after
returnhas 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, asdoubledid above; passing individual values builds a fresh slice. -
There is no automatic conversion between slice types: a
[]stringcannot be passed as...any, and the loop that builds a[]anyis 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)andfunc([]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
Source: A Tour of Go: Function closures
The four lines show the whole model:
-
Each call to
countercreates a freshn, soaandbare independent. -
A closure captures the variable, not its value: after
x = 2,showprints 2. -
Since Go 1.22, a
forloop’s variables are per-iteration, so each closure appended in the three-clause loop captures its owniand prints0 1 2. -
A variable declared outside the loop (
shared) is a single variable no matter what the loop does, so every closure sees its final value3.
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 ( |
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 |
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.
Source: Go 1.27 Release Notes: Compiler
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
Source: slices: SortFunc
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 |
|---|---|---|
|
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 |
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, passTfor small structs or*Tfor large ones. -
A function that grows a slice must return it (
xs = append(xs, v)), becauseappendmay 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]intor 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:
-
Arguments, and the function value and receiver, are evaluated when the
deferstatement executes, not when the call runs. That is whydefer trace("work")()prints "enter" immediately and the returned closure prints "exit" last. -
Deferred functions run in LIFO order after the surrounding function’s result parameters have been set.
-
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>
Source: Effective Go: Defer
Common defer mistakes
-
Evaluating too early:
defer log.Println("elapsed:", time.Since(start))evaluatestime.Since(start)at thedeferstatement, so it prints (almost) zero.go vetreports it with thedefersanalyzer. Wrap it in a closure to measure at exit. -
Deferring a call that has no effect on exit, for example
defer fwith 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.Exitorlog.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.
Source: Go 1.14 Release Notes: Runtime
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):
-
The current function stops. Its deferred calls run, in LIFO order.
-
The panic unwinds to the caller, whose deferred calls run, and so on up the goroutine’s stack.
-
If a deferred function calls
recover()directly, the panic stops:recoverreturns the value passed topanic, 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). -
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.
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:
-
It only has an effect when called directly by a deferred function. A helper that calls
recover()and is called from the deferred function seesnil. -
It only returns a non-nil value while the goroutine is panicking and the deferred function was run by the panic’s unwinding.
-
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
Source: runtime: PanicNilError
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, theMust...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
Source: Effective Go: Recover
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
Source: sync: WaitGroup.Go
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 |
|---|---|
|
A function with results whose body does not end in a terminating statement. |
|
A naked return after an inner block shadowed a named result. |
|
|
|
Wrong number or type of arguments, including |
|
The |
|
The |
|
A |
|
Formatting, and modernizers that remove the pre-1.22 |
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,
deferand the advice to avoid naked returns. The loop-variable pitfall it touches on, and the copy idiom taught in theforand goroutine chapters (4 and 10), are obsolete in modules declaringgo 1.22or 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/recoverdiscussion predates the*runtime.PanicNilErrorbehaviour of Go 1.21, sorecover()is now nevernilafter a panic. Several of its examples treat an ordinary failure (a file that already exists) as a panic and do not check theCloseerror 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.Asand theerrors.Joinof Go 1.20 available for the error side. -
Neither book covers the generic
Map/Filter/Reducehelpers that are plain library code since Go 1.18 (Bodner’s generics chapter predates the final design), theslicesandmapspackages (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 |
|
Go 1.18 |
Generics, so higher-order helpers can be typed. |
Go 1.20 |
|
Go 1.21 |
|
Go 1.22 |
Per-iteration loop variables (modules with |
Go 1.23 |
Range-over-function iterators ( |
Go 1.25 |
|
Go 1.26 |
|
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. |
Source: Go 1.27 Release Notes