Back to Notes

Go Functions

Overview

Functions are first-class citizens in Go — they can be assigned to variables, passed as arguments, and returned from other functions. Types come after variable names. Go natively supports multiple return values, making error handling idiomatic without exceptions.


Basic Syntax

The type is written after the parameter name. If consecutive parameters share a type, you can omit all but the last.

// Full form
func add(a int, b int) int {
    return a + b
}

// Shortened — shared type
func add(a, b int) int {
    return a + b
}

fmt.Println(add(3, 4))  // 7

Multiple Return Values

Functions can return more than one value — no need to wrap in a struct or use output parameters. The convention is to return (result, error) as the last pair.

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

result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result)  // 5

Blank Identifier _

Use _ to discard return values you don't need — Go requires all declared variables to be used, so _ is the escape hatch.

result, _ := divide(10, 2)  // ignore error (only when you're certain it won't error)

// Also works with range
for _, v := range nums { fmt.Println(v) }

Named Return Values

Named returns declare variables at the function signature level. A bare return (naked return) sends back their current values. Useful for short functions; hurts readability in long ones.

// Named returns: x and y are pre-declared
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return   // returns x and y — naked return
}

a, b := split(17)  // a=7, b=10

Rule: Only use naked returns in very short functions. In longer functions, name the returns for documentation but use explicit return x, y.


Anonymous Functions

A function without a name. Used as a value — assign to a variable, pass as argument, or invoke immediately.

// Assign to variable
double := func(n int) int {
    return n * 2
}
fmt.Println(double(5))  // 10

// Pass as argument
apply := func(nums []int, fn func(int) int) []int {
    result := make([]int, len(nums))
    for i, v := range nums {
        result[i] = fn(v)
    }
    return result
}
fmt.Println(apply([]int{1, 2, 3}, double))  // [2 4 6]

// Immediately Invoked Function Expression (IIFE)
result := func(a, b int) int {
    return a + b
}(3, 4)
fmt.Println(result)  // 7

Closures

A closure is a function that captures variables from its surrounding scope. The captured variables persist and are shared between the closure and the outer function — they're not copied.

// Counter factory — each call to makeCounter returns an independent counter
func makeCounter() func() int {
    count := 0             // captured by the closure
    return func() int {
        count++
        return count
    }
}

c1 := makeCounter()
c2 := makeCounter()

fmt.Println(c1())  // 1
fmt.Println(c1())  // 2
fmt.Println(c2())  // 1 — independent from c1
// Adder factory
func makeAdder(x int) func(int) int {
    return func(y int) int {
        return x + y   // x is captured from makeAdder's scope
    }
}

addFive := makeAdder(5)
fmt.Println(addFive(3))   // 8
fmt.Println(addFive(10))  // 15

Variadic Functions

A variadic function accepts any number of arguments of a given type using .... Inside the function, the variadic parameter is a slice.

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

sum(1, 2, 3)              // 6
sum(1, 2, 3, 4, 5)        // 15

// Spread a slice into a variadic call
nums := []int{1, 2, 3}
sum(nums...)               // 6

fmt.Println is variadic: func Println(a ...any) (n int, err error)


defer — Delayed Execution

defer schedules a function call to run just before the surrounding function returns, regardless of how it returns (normal, error, panic). Arguments are evaluated immediately, but execution is delayed.

Typical uses: closing files, releasing locks, cleanup, logging.

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()   // guaranteed to run when readFile returns

    // ... read from f
    return nil
}
func main() {
    ans := 10
    defer fmt.Println("deferred:", ans)  // ans captured = 10 right now
    ans = 100
    fmt.Println("main:", ans)
}
// main: 100
// deferred: 10

Rule 1 — Arguments evaluated immediately

func a() {
    i := 0
    defer fmt.Println(i)  // captures i=0 now
    i++
    // prints 0, not 1
}

Rule 2 — LIFO (last in, first out)

Multiple defers run in reverse order — like a stack:

func b() {
    defer fmt.Print("1 ")  // runs third
    defer fmt.Print("2 ")  // runs second
    defer fmt.Print("3 ")  // runs first
}
// Output: 3 2 1

// Practical: acquire in order, release in reverse
defer mu.Unlock()   // runs first (last deferred)
defer f.Close()     // runs second
defer db.Close()    // runs third

Rule 3 — Can read/write named return values

func double(n int) (result int) {
    defer func() {
        result *= 2   // modifies the named return after return
    }()
    result = n
    return   // returns n*2, not n
}

fmt.Println(double(5))  // 10

Functions as Values (First-Class)

Functions have types and can be stored in variables, maps, slices, or passed around like any other value.

// Function type
type MathFunc func(int, int) int

func apply(a, b int, fn MathFunc) int {
    return fn(a, b)
}

add := func(a, b int) int { return a + b }
mul := func(a, b int) int { return a * b }

fmt.Println(apply(3, 4, add))  // 7
fmt.Println(apply(3, 4, mul))  // 12

// Dispatch table
ops := map[string]MathFunc{
    "+": add,
    "*": mul,
}
fmt.Println(ops["+"](10, 5))  // 15

Gotchas

GotchaDetail
defer LIFOLast registered defer runs first
defer args evaluated nowdefer fmt.Println(x) captures x's current value
Naked return in long functionsHard to reason about — use explicit returns
Closures capture by referenceAll iterations of a loop closure share the same loop variable (Go < 1.22) — use v := v to copy
_ doesn't suppress errors silentlyUse it only when you're certain the value is truly irrelevant