Skip to content

Functions

A Go function declares its parameter types and return types explicitly. The return type appears after the parameter list:

func add(a int, b int) int {
return a + b
}
// Consecutive parameters of the same type can share a type annotation.
// This is a second, equivalent spelling of the same function — pick one style per function.
func add(a, b int) int {
return a + b
}

Every argument in Go is passed by value — a copy of it — with no exception. That is easy to see for an int or a struct, but it is true for a slice too: a slice argument copies its small three-word header (pointer, length, capacity), not the backing array. That is why writing to s[i] inside a function is visible to the caller, while append-ing inside the function sometimes is not — the full explanation, with the header diagram, is in Slices.

Go functions can return more than one value. The idiomatic pattern is (result, error) — the caller must handle or explicitly discard the error:

func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 3)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)

Return values can be named. Named returns pre-declare variables and allow a bare return (a return with no arguments that returns the current values of the named variables). Use them sparingly — they are clearest in short functions where the names genuinely document the result:

func bounds(nums []int) (lo, hi int) {
lo, hi = nums[0], nums[0]
for _, v := range nums[1:] {
if v < lo {
lo = v
}
if v > hi {
hi = v
}
}
return // bare return: returns current lo, hi
}

Naming named results min and max would also compile — Go 1.21’s built-in min/max functions can be shadowed by a local name — but it is a smell: it hides the built-ins from the rest of the function body for no benefit.

A variadic function accepts a variable number of arguments of the same type. Declare the last parameter with ...:

func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // individual args
sum(1, 2, 3, 4, 5) // any number of args works
sum([]int{1,2,3}...) // spread a slice with ...

Inside the function, nums is a []int slice. Spreading a nil slice works too and is not a special case: var none []int; sum(none...) passes an empty slice, and the loop simply runs zero times, returning 0. A nil slice behaves like a zero-length one everywhere it is read.

defer schedules a function call to run when the surrounding function returns, regardless of which return path is taken. It is the idiomatic way to clean up resources:

func readFile(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close() // runs when readFile returns
// ... work with f ...
return nil
}

Multiple deferred calls execute in LIFO order (last deferred, first to run).

flowchart TB
  subgraph Push["Pushed in this order"]
    direction LR
    P1["defer close(0)"] --> P2["defer close(1)"] --> P3["defer close(2)"]
  end
  subgraph Pop["Run in this order at return (LIFO)"]
    direction LR
    R1["close(2)"] --> R2["close(1)"] --> R3["close(0)"]
  end
Three defers run in reverse order of when they were pushed

Arguments to a deferred call are evaluated immediately, at the defer statement — not when the deferred call actually runs:

i := 0
defer fmt.Println(i) // captures i's value (0) right now
i = 100
// when the function returns, this prints "0", not "100"

A deferred closure can read and even rewrite a named return value, because the named result is just a variable in scope for the whole function body — including its deferred calls. This is the standard way to annotate or transform an error right before it leaves a function:

func doWork() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("doWork: %w", err)
}
}()
return errors.New("boom")
}

doWork() returns errors.New("boom"), but by the time the caller sees it, the deferred function has already run and wrapped it — the call prints doWork: boom. This only works with a named result: a deferred function closing over a plain return err cannot change what the caller receives, because there is no shared variable for it to modify.

defer runs at function return, not at the end of the loop iteration it was written in. Deferring inside a for loop is a classic source of resource leaks — every file, connection, or lock accumulates until the whole function exits, not each time around the loop:

func leaky(n int) {
for i := 0; i < n; i++ {
r := openResource(i)
defer r.Close() // accumulates n deferred calls, all pending until leaky returns
}
// if n is large, this line runs with n resources still open
}

If leaky is itself a long-running function — say, one that processes thousands of files in a loop — every open file descriptor stays open until the very end, and the process can run out of them long before leaky returns. The fix is to give each iteration its own scope, typically a helper function, so defer fires per iteration:

func tidy(n int) {
for i := 0; i < n; i++ {
func() {
r := openResource(i)
defer r.Close() // runs at the end of this anonymous function, every iteration
// ... use r ...
}()
}
}

A closely related bug is defer f.Close() silently discarding the error Close returns. For a read-only file this is usually harmless, but for a file you wrote to, Close can be the call that actually flushes buffered data — and a failed flush is a real error you just threw away:

func saveBuggy(f *os.File, data []byte) error {
defer f.Close() // any error from Close is silently discarded
_, err := f.Write(data)
return err
}
func saveFixed(f *os.File, data []byte) (err error) {
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr // only report Close's error if Write didn't already fail
}
}()
_, err = f.Write(data)
return err
}

Functions are first-class values in Go. You can assign them to variables, pass them as arguments, and return them from other functions:

// Function as a value in a variable
double := func(n int) int { return n * 2 }
fmt.Println(double(5)) // 10
// Function as a parameter
apply := func(f func(int) int, x int) int { return f(x) }
fmt.Println(apply(double, 3)) // 6

A closure is a function that captures variables from its enclosing scope. The captured variable is shared — the closure sees mutations to it:

func counter() func() int {
n := 0
return func() int {
n++ // captures n from counter
return n
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2 — state is preserved inside the closure
fmt.Println(c()) // 3

Each call to counter() creates an independent n, so two counters do not interfere with each other.

Closures over loop variables: before and after Go 1.22

Section titled “Closures over loop variables: before and after Go 1.22”

A closure created inside a for loop captures the loop variable itself, not a snapshot of its value at that iteration. Before Go 1.22, all iterations of a for shared one variable, so every closure ended up seeing whatever the variable held when it was finally called:

var fns []func()
for _, v := range []int{0, 1, 2} {
fns = append(fns, func() { fmt.Println(v) })
}
for _, f := range fns {
f()
}

Compiled with go 1.21 in go.mod, this program prints 2 2 2 on Go 1.26.5 — every closure shares the same v, whose final value after the loop is 2. The traditional fix, still common in code written before 1.22 (and in anything an AI trained on that code produces), is to make a fresh copy inside the loop body: v := v before using v in the closure.

Go 1.22 changed the semantics: each iteration gets its own v, so the same source line — with only go.mod’s go directive changed to 1.22 — prints 0 1 2:

go 1.21 in go.mod → 2
2
2
go 1.22 in go.mod → 0
1
2

Both lines above are real output from this exact program on Go 1.26.5, changing only the go line in go.mod between runs. The interactive playground below always targets a modern toolchain, so it will show the post-1.22 behavior; the go directive is what gates this change, per module, and it cannot be toggled from inside a single program. The v := v copy is unnecessary once a module’s go.mod says go 1.22 or later — but you will still see it in older code, and recognizing it tells you the author was guarding against the old behavior.

A function type is just another type — you can declare a variable of it, store it in a struct field, or pass it as a value. Its zero value is nil, and a function value can only ever be compared against nil (comparing two non-nil function values with == is a compile error):

type IntFunc func(int) int
var f IntFunc
fmt.Println(f == nil) // true
f(3) // panics: runtime error: invalid memory address or nil pointer dereference

This is the same shape of bug as a nil pointer dereference, and it shows up most often with optional callback fields on a struct (OnError func(error)) that a caller forgot to set. Always nil-check a function value before calling it unless the type’s contract guarantees it is always set.

package main
import (
"errors"
"fmt"
)
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Variadic
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Closure
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
// defer demo
func deferDemo() {
fmt.Println("deferDemo start")
defer fmt.Println("deferred 1 (runs last)")
defer fmt.Println("deferred 2 (runs second)")
defer fmt.Println("deferred 3 (runs first)")
fmt.Println("deferDemo end")
}
func main() {
// Multiple returns
result, err := divide(10, 3)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Printf("10 / 3 = %.4f\n", result)
}
_, err = divide(5, 0)
if err != nil {
fmt.Println("divide by zero:", err)
}
// Variadic
fmt.Println("sum(1..5) =", sum(1, 2, 3, 4, 5))
// Closure counter
c := counter()
fmt.Println("counter:", c(), c(), c())
// defer LIFO
deferDemo()
}

The second playground covers everything new on this page: named results modified by defer, defer inside a loop, a swallowed Close error and its fix, and nil function values.

package main
import (
"errors"
"fmt"
)
// doWork shows a named result modified by a deferred func — a common way to
// annotate an error right before it leaves the function.
func doWork() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("doWork: %w", err)
}
}()
return errors.New("boom")
}
type resource struct{ id int }
func openResource(id int) *resource {
fmt.Println("open", id)
return &resource{id}
}
func (r *resource) Close() { fmt.Println("close", r.id) }
// leaky defers Close inside a loop — every resource stays open until leaky
// itself returns, not at the end of each iteration.
func leaky(n int) {
for i := 0; i < n; i++ {
r := openResource(i)
defer r.Close() // accumulates n deferred calls
}
fmt.Println("all opened, none closed yet")
}
type mockFile struct{ closeErr error }
func (f *mockFile) Write(p []byte) (int, error) { return len(p), nil }
func (f *mockFile) Close() error { return f.closeErr }
// saveBuggy discards whatever error Close returns.
func saveBuggy(f *mockFile, data []byte) error {
defer f.Close() // any error from Close is silently discarded
_, err := f.Write(data)
return err
}
// saveFixed captures Close's error into the named result if nothing else failed.
func saveFixed(f *mockFile, data []byte) (err error) {
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
_, err = f.Write(data)
return err
}
type IntFunc func(int) int
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
// Named result modified by defer.
fmt.Println(doWork())
// defer inside a loop: resources pile up until the function returns.
leaky(3)
// defer f.Close() silently swallowing a write/flush error, and the fix.
f := &mockFile{closeErr: errors.New("disk full")}
fmt.Println("saveBuggy:", saveBuggy(f, []byte("data")))
fmt.Println("saveFixed:", saveFixed(f, []byte("data")))
// A nil function value compares equal to nil, and calling it panics.
var nilFn IntFunc
fmt.Println("nil func == nil:", nilFn == nil)
// A nil slice spread into a variadic parameter behaves like an empty one.
var nilSlice []int
fmt.Println("sum(nilSlice...):", sum(nilSlice...))
}
What you getBenefitCost
Multiple return valuesExplicit error handling, no hidden exceptionsThe caller must check every error — more verbose than try/catch
Named return valuesDocuments intent in the signature, enables bare return, lets defer rewrite the resultA bare return in a long function makes it harder to read
deferCleanup code sits right next to resource acquisitionArguments are evaluated immediately, LIFO order can surprise you, and it runs at function exit, not loop-iteration exit
ClosuresEncapsulate state conciselyCaptured variables are often allocated on the heap (escape analysis)
Pass-by-value everywhereNo hidden aliasing through function boundaries by defaultPassing a large struct by value copies all of it; a slice or map argument still lets you mutate what it points to
  • “defer is slow.” defer has very low overhead (nanosecond range) — it is safe to use in production code for cleanup.
  • “Multiple return values hurt performance.” Go returns multiple values directly via registers or the stack, with no heap allocation — the performance impact is negligible.
  • “Every closure escapes to the heap.” The compiler performs escape analysis and may allocate a closure on the stack if it never escapes its scope.
  • “Functions in Go are not first-class.” Functions in Go are first-class values — you can assign them, pass them as arguments, or return them from other functions.
  • “defer inside a loop cleans up each iteration.” It does not — every deferred call is pending until the enclosing function returns, not the loop.

When a generated function uses defer, closures, or function values, check these before trusting it:

  • Does a defer sit inside a for loop that can run more than a couple of times? Every iteration’s deferred call stays pending until the function returns, not the loop.
  • Does defer f.Close() follow a Write/Flush-style call, discarding whatever error Close returns? Look for a named result the deferred func could attach that error to instead.
  • Does a closure capture a loop variable, and if so, does the module’s go.mod declare go 1.22 or later? Below that, every closure created in the loop shares one variable.
  • Is a function-typed value (a field, a parameter) called without checking it against nil first?
  • Does the code compare two function values directly with == (not against nil)? That is a compile error — function types support no other comparison.

🐛 Spot the bug

func leaky(n int) {
for i := 0; i < n; i++ {
r := openResource(i)
defer r.Close()
}
fmt.Println("all opened, none closed yet")
}
Show the answer

defer schedules r.Close() to run when leaky returns — not at the end of each loop iteration. With n large, this line runs with all n resources still open, and in a long-running program that can exhaust file descriptors or connections long before leaky returns. Fix it by giving each iteration its own scope so the deferred call fires per iteration, typically with a small helper function: func() { r := openResource(i); defer r.Close(); /* use r */ }() called once per loop iteration.

In Go, which idiomatic pattern do functions use when they can fail?
Three defer statements are executed in a function in order A, B, C. In what order do they run when the function returns?
What does a closure capture from its enclosing scope?
How do you pass a slice to a variadic function as individual arguments?
What does fmt.Println(doWork()) print?
func doWork() (err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("doWork: %w", err)
        }
    }()
    return errors.New("boom")
}
At the moment "done opening" is printed, how many resources are still open?
for i := 0; i < 3; i++ {
    r := openResource(i)
    defer r.Close()
}
fmt.Println("done opening")