13. Multiple return values

📖 Reading · 9 min
💡 Every code box below is live — edit it and hit Run.

Most languages let a function hand back exactly one thing. Go lets it hand back several — and that single decision shapes the way all Go code is written, especially error handling.

Two results instead of one

package main

import "fmt"

func minMax(a, b int) (int, int) {
    if a < b {
        return a, b
    }
    return b, a
}

func main() {
    lo, hi := minMax(9, 4)
    fmt.Println("low:", lo, "high:", hi)
}

The result types go in parentheses: (int, int). The return statement lists both values, and the caller receives both with a single :=.

No wrapper struct, no out-parameters, no tuple type you have to unpack — just two values.

The pattern that runs Go: (value, error)

Here is the convention you will see in essentially every Go library ever written. A function that might fail returns its 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, 4)
    if err != nil {
        fmt.Println("failed:", err)
    } else {
        fmt.Println("result:", result)
    }

    _, err = divide(1, 0)
    if err != nil {
        fmt.Println("failed:", err)
    }
}

Read the shape carefully, because you'll type it a thousand times:

  1. On failure, return the zero value for the result plus a non-nil error.
  2. On success, return the real result plus nil.
  3. The caller checks if err != nil immediately, before touching the result.

Go has no exceptions for ordinary failures. Nothing is thrown past you; errors are values you get back and deal with on the next line. Module 7 goes deep on what an error actually is — for now, just get comfortable with the two-value handshake.

Ignoring a result with _

Sometimes you genuinely don't want one of the values. The blank identifier _ throws it away:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    n, _ := strconv.Atoi("42")
    fmt.Println("n is", n)

    _, err := strconv.Atoi("not a number")
    fmt.Println("err is:", err)
}

strconv.Atoi ("ASCII to integer") is itself a (value, error) function — the standard library follows the same convention your code does.

Use _ when you have thought about the value and decided you don't need it. Discarding an err with _ is a code smell in real projects: it's how bugs hide. Discarding it is a decision, not a shortcut.

Named results

You can name the results in the signature. The names become ordinary variables, pre-initialised to their zero values:

package main

import "fmt"

func split(total int) (hours, minutes int) {
    hours = total / 60
    minutes = total % 60
    return hours, minutes
}

func main() {
    h, m := split(195)
    fmt.Printf("%dh %dm\n", h, m)
}

Named results serve one purpose better than any other: documentation. func split(total int) (hours, minutes int) tells you which number is which, while (int, int) leaves you guessing.

Go also allows a bare return with named results — it returns whatever the named variables currently hold. It's legal, and it's how deferred functions modify a return value (a trick you'll meet in the errors module), but in ordinary code, spell out what you're returning. A bare return at the bottom of a long function is a puzzle for the reader.

Three is fine, six is a smell

Nothing stops you returning more:

package main

import "fmt"

func stats(nums []int) (sum, count, max int) {
    for i, n := range nums {
        sum += n
        count++
        if i == 0 || n > max {
            max = n
        }
    }
    return sum, count, max
}

func main() {
    s, c, m := stats([]int{3, 9, 4})
    fmt.Println("sum:", s, "count:", c, "max:", m)
}

But past two or three, callers start losing track of the order. When you get there, return a struct instead and give every field a name — that's what the Structs module is for.

Your turn

Write applyTax that takes a float64 price and returns the tax amount (10% of price) and the total price, in that order. Print them with two decimals so the program outputs exactly:

tax 5.00, total 55.00
package main

import "fmt"

// write applyTax here — it returns (tax, total)

func main() {
    tax, total := applyTax(50)
    fmt.Printf("tax %.2f, total %.2f\n", tax, total)
}
package main

import "fmt"

func applyTax(price float64) (float64, float64) {
    tax := price * 0.10
    return tax, price + tax
}

func main() {
    tax, total := applyTax(50)
    fmt.Printf("tax %.2f, total %.2f\n", tax, total)
}

Next: functions that accept any number of arguments.