03 / 20 · Day 1
Day 1 · Concept 03

Functions

Go functions look superficially like C functions — until they don't. They return multiple values (the canonical (result, err) pair), they're first-class (pass them around), and they support closures. Once those click, half of Go's standard library reads differently.


1 · The intuition

Most languages return one value from a function. If you want more, you wrap them in a tuple, a struct, an out-parameter, or a callback. Go just lets the function return more than one. This sounds like a small detail; in practice it's the pivot point of Go's whole error-handling philosophy.

The canonical Go function signature is func foo(x int) (result T, err error) — the result and the error are siblings. Every function that can fail returns both; the caller checks err first. No exceptions. No try / catch. No Result<T, E> generic. Just two return values.

2 · Try it — the canonical shape

package main

import (
    "errors"
    "fmt"
)

// divmod returns quotient and remainder, plus an error if b is zero.
func divmod(a, b int) (int, int, error) {
    if b == 0 {
        return 0, 0, errors.New("division by zero")
    }
    return a / b, a % b, nil  // nil error means "all good"
}

func main() {
    q, r, err := divmod(17, 5)
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    fmt.Printf("17 / 5 = %d remainder %d\n", q, r)

    // Try the failure case
    _, _, err = divmod(10, 0)
    if err != nil {
        fmt.Println("expected:", err)
    }
}

Three return values; the third is error by convention. The blank identifier _ discards the values we don't care about — without it, Go would complain about unused variables.

3 · Named return values — when they help

You can give the return values names in the signature. They start as zero values and you can just return with no arguments to send them back. Useful for documentation; controversial for everything else.

// Named returns — q, r, err are pre-declared.
func divmod2(a, b int) (q, r int, err error) {
    if b == 0 {
        err = errors.New("division by zero")
        return                  // "naked return" — returns q, r, err
    }
    q = a / b
    r = a % b
    return                       // again, naked
}
The rule of thumb. Use named returns when the names document the return values (especially in API docs) — like func (r *Reader) ReadByte() (c byte, err error) in io. Don't use them just to avoid typing the return statement. Don't use them with naked returns in functions longer than ~5 lines — they obscure the flow.

4 · Variadic functions

A function can accept any number of arguments of one type using ...T. The classic example is fmt.Println. You can write your own.

// sum accepts any number of ints. Inside the function, nums is a []int.
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3))           // 6
    fmt.Println(sum(10, 20, 30, 40))    // 100

    // Pass a slice as variadic with the ... spread
    xs := []int{5, 5, 5}
    fmt.Println(sum(xs...))              // 15
}

5 · First-class functions & closures

Functions in Go are values. You can store them in variables, pass them as arguments, and return them from other functions. They can close over outer variables — exactly like a closure in JavaScript or Python.

// Pass a function as an argument
func apply(x int, fn func(int) int) int {
    return fn(x)
}

func double(x int) int { return x * 2 }
func square(x int) int { return x * x }

func main() {
    fmt.Println(apply(5, double))    // 10
    fmt.Println(apply(5, square))    // 25

    // Anonymous function (function literal)
    fmt.Println(apply(5, func(x int) int { return x + 100 }))  // 105
}

Closures — counter factory

// makeCounter returns a function that closes over 'count'.
func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    next := makeCounter()
    fmt.Println(next())  // 1
    fmt.Println(next())  // 2
    fmt.Println(next())  // 3

    // Each call to makeCounter() gives a fresh closure with its own count
    other := makeCounter()
    fmt.Println(other()) // 1 — independent
}

6 · defer — the surprise

defer schedules a function to run when the enclosing function returns. It's how Go does "cleanup at the end" without try-finally. Multiple defers run in reverse order (LIFO) — last deferred, first run.

func readFile(path string) (string, error) {
    f, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer f.Close()         // runs when readFile returns, even on panic

    // ... read from f ...
    return result, nil
}

// Multiple defers — LIFO order
func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    fmt.Println("body")
}
// Output:
//   body
//   3
//   2
//   1
The common bug. defer evaluates arguments immediately, but runs the call later. defer fmt.Println("x=", x) captures x's current value, not its value at function exit. If you want the latest value, wrap in a closure: defer func() { fmt.Println("x=", x) }().

7 · Common mistakes

  • Forgetting to check err. The if err != nil pattern feels repetitive — but skipping it produces bugs the type system can't catch. The compiler doesn't force you; the conventions do.
  • Returning a pointer to a stack variable. In Go, this is fine — the compiler does escape analysis and moves the variable to the heap automatically. Coming from C, it feels wrong; in Go, it's idiomatic.
  • Mutating a closed-over loop variable. Classic gotcha: for i := range xs { defer func() { fmt.Println(i) }() } — all defers print the same final i. Fix: pass it as an argument, or shadow with i := i inside the loop. (Since Go 1.22 this specific case was fixed for for loops, but the issue still applies elsewhere.)
  • Naked returns past 5 lines. Named returns + naked return in a long function makes it impossible to tell what's being returned. Either return explicitly or shrink the function.
  • Functions that take many arguments. Go conventions: 4+ arguments is a signal to introduce a struct. func New(name string, age int, addr string, phone string, email string, isAdmin bool) begs to be func New(opts UserOpts).

8 · When it clicks

  • You write (result, error) returns by reflex.
  • You spot when defer is the right tool (file close, mutex unlock, panic recovery) versus when it's overkill.
  • You can read a closure that returns a closure and not flinch.
  • You know that func(int) int is a type, not a syntax oddity.
Found this useful?