Skip to content

04 · Functions & Multiple Returns

🎥 Video walkthrough

Basic function syntax

package main

import "fmt"

// name(params) returnType
func add(a int, b int) int {
    return a + b
}

// consecutive parameters of the same type can share one annotation
func multiply(a, b int) int {
    return a * b
}

func main() {
    fmt.Println(add(2, 3))      // 5
    fmt.Println(multiply(4, 5)) // 20
}

Multiple return values

This is one of Go's signature features — functions routinely return more than one value, most commonly a result and an error:

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("error:", err)
    } else {
        fmt.Println("result:", result)
    }

    _, err = divide(10, 0)
    if err != nil {
        fmt.Println("error:", err) // error: division by zero
    }
}

The blank identifier _ discards a return value you don't need — Go forces you to either use every declared variable or explicitly discard it.

Named return values

Return values can be named up front; a bare return sends back their current values. Useful for short functions and for documenting intent, but overusing it can hurt readability in longer functions.

package main

import "fmt"

func divmod(a, b int) (quotient, remainder int) {
    quotient = a / b
    remainder = a % b
    return // "naked" return -- sends back quotient, remainder
}

func main() {
    q, r := divmod(17, 5)
    fmt.Println(q, r) // 3 2
}

Variadic functions

A parameter prefixed with ... accepts zero or more arguments, collected into a slice inside the function:

package main

import "fmt"

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

func main() {
    fmt.Println(sum())           // 0
    fmt.Println(sum(1, 2, 3))    // 6

    values := []int{10, 20, 30}
    fmt.Println(sum(values...)) // spread a slice with ...
}

Functions as values, closures

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

package main

import "fmt"

// takes a function as a parameter
func applyTwice(f func(int) int, x int) int {
    return f(f(x))
}

// returns a closure that captures "start"
func counterFrom(start int) func() int {
    count := start
    return func() int {
        count++
        return count
    }
}

func main() {
    double := func(x int) int { return x * 2 }
    fmt.Println(applyTwice(double, 3)) // 12

    next := counterFrom(10)
    fmt.Println(next()) // 11
    fmt.Println(next()) // 12
    fmt.Println(next()) // 13
}

defer — run code on function exit

defer schedules a call to run right before the surrounding function returns, regardless of how it returns. It's the idiomatic way to guarantee cleanup (closing files, unlocking mutexes) sits right next to the code that acquired the resource.

package main

import "fmt"

func process() {
    fmt.Println("start")
    defer fmt.Println("cleanup (runs last)")
    fmt.Println("middle")
    // "cleanup" prints after "middle", right before process() returns
}

func main() {
    process()
    // start
    // middle
    // cleanup (runs last)
}

Multiple defer calls run in LIFO order (last deferred, first executed).

How It Actually Works

Go doesn't fake multiple return values with a tuple object the way Python does — the ABI (calling convention) reserves stack slots (or, on newer Go versions using the register-based ABI, specific registers) for each return value, and the caller reads them directly with no allocation or packing step. That's why returning (int, error) costs nothing extra over returning just int: the second value is just another slot filled in before ret. defer doesn't run "later" in some vague sense — the compiler maintains a per-goroutine linked list of deferred calls attached to the current stack frame, and RET is compiled to first pop and execute every entry on that list (LIFO) before actually returning to the caller. A deferred function can still read and modify named return values because those return values are addressable stack slots that stay alive until the real return, which is the mechanism behind the classic "recover and set an error in a deferred func" pattern. Named vs. unnamed returns compile to identical code otherwise — naming just gives you a variable to write to from a defer.

Cheat sheet

Feature Syntax
Basic function func add(a, b int) int { return a + b }
Multiple returns func f() (int, error) { ... }
Named returns func f() (x int) { x = 1; return }
Variadic func f(nums ...int) { ... }
Spread a slice f(values...)
Discard a return _, err := f()
Function value var f func(int) int = double
Closure func() func() int { ... }
Deferred cleanup defer file.Close()

🔀 See this in another language

Exercise

Write a function minMax(nums ...int) (min, max int) using named return values that finds the smallest and largest numbers in a variadic list of ints (return 0, 0 for an empty call). In main, call it with a slice spread using ..., and add a defer statement that prints "done" after the results are printed.