Foundations View as Markdown

Functions

Multiple return values, named results, variadics, closures — and the error convention that shapes every Go API.

go
func add(a int, b int) int {
    return a + b
}

func addSub(a, b int) (int, int) {   // same type? declare it once
    return a + b, a - b
}

Multiple return values#

This is the feature that defines Go's style. Almost every function that can fail returns two values, and the second is an error.

go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide %v by zero", a)
    }
    return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
    return fmt.Errorf("compute ratio: %w", err)
}

Three conventions worth learning immediately:

  • The error is always the last return value.
  • On error, other return values are their zero value and must not be used.
  • %w in fmt.Errorf wraps the error so errors.Is and errors.As can find it later. %v does not — that difference is invisible in the message and breaks callers.

Named return values#

go
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return                 // "naked" return — returns x and y
}

Useful for documenting what each value means, and useful with defer for modifying a result on the way out. Avoid naked returns in anything longer than a few lines: the reader has to scroll to find what is actually returned.

Variadic functions#

go
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)
sum(values...)             // spread a slice

Functions are values#

go
func apply(nums []int, f func(int) int) []int {
    out := make([]int, len(nums))
    for i, n := range nums {
        out[i] = f(n)
    }
    return out
}

doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })

Closures#

go
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

next := counter()
next()   // 1
next()   // 2

Each call to counter gets its own count.

defer#

defer schedules a call to run when the surrounding function returns — including when it returns because of a panic.

go
func readConfig(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("open %s: %w", path, err)
    }
    defer f.Close()          // runs no matter which return we take
    return io.ReadAll(f)
}

Deferred calls run last-in-first-out, and their arguments are evaluated immediately — at the defer line, not when it runs. That trips people up exactly once.

Common questions#

Why does Go not have default parameter values?#

Deliberate simplicity: there is one calling convention and no overload resolution to reason about. The idiomatic replacements are a config struct, or functional options (WithTimeout(5*time.Second)) when a package needs many optional settings.

When should I use named return values?#

When the names document something the types do not — (width, height int) — or when you need defer to modify the result, as in the close example above. Otherwise prefer explicit returns.

Is defer expensive?#

Not meaningfully since Go 1.14; it is close to free in the common case. Use it. The one place to think about it is a defer inside a hot loop, where it accumulates until the function returns rather than the loop iteration.

Get the Go agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.