Multiple Return Values

In Go, a function can return more than one value from a single call — no wrapping in a struct, tuple, or array required. This is built directly into the language: the function signature simply lists more than one return type, and the caller receives them as a comma-separated group in one assignment statement. Multiple return values are the backbone of one of Go’s most distinctive habits: functions that hand back a result and an error side by side, so failure is just another value you check instead of an exception you catch.

Overview / How It Works

A Go function is not limited to a single result. Its signature can list two, three, or more return types in parentheses, and every return statement in the function must supply exactly that many values, in that order. There is no tuple type involved and no wrapper object gets allocated for you — the compiler treats the return list as multiple distinct values that are handed back to the caller and assigned to multiple variables in a single statement, using the familiar := or = multiple-assignment syntax.

This exists largely because Go deliberately has no exceptions for routine error handling. Instead of throwing and catching, a Go function that can fail simply returns its normal result alongside an error value: func Do() (Result, error). The caller receives both, checks err first, and only trusts the result when err is nil. This convention is so pervasive that nearly every standard-library function that can fail follows it — os.Open, strconv.Atoi, json.Marshal, and thousands more.

The same two-value shape also appears in a few built-in expressions that are not ordinary function calls: value, ok := m[key] checks whether a key exists in a map, value, ok := x.(SomeType) performs a safe type assertion instead of panicking, and value, ok := <-ch reports whether a channel receive produced a value before the channel was closed. All of these reuse the same "result plus a trustworthiness flag" pattern that multiple return values make possible.

Under the hood, Go’s calling convention passes return values back to the caller either in registers or on the stack, depending on the compiler and target platform — an implementation detail you never manage yourself. What matters at the language level is simpler: the number and order of values in the return statement must exactly match the function’s declared return types, and the caller must account for all of them, either by assigning each to a variable or by explicitly discarding ones it doesn’t need with the blank identifier _.

Go also lets you name your return values directly in the signature, for example func findRange(nums []int) (lo, hi int). Named return values are pre-declared, zero-valued variables that live for the whole function body; you can assign to them like ordinary local variables, and a bare return statement (with no values listed) sends back whatever they currently hold. This is handy for documenting what each return position means, but overusing bare returns in long or branching functions can make the code harder to follow, since the reader has to scroll back to the signature to see what’s actually being sent back.

Syntax

The general form of a function with multiple return values:

func functionName(param1 Type1, param2 Type2) (ReturnType1, ReturnType2) {
    // ... function body
    return value1, value2
}
Part Meaning
(param1 Type1, param2 Type2) The normal parameter list, exactly like any function.
(ReturnType1, ReturnType2) The list of return types, wrapped in parentheses because there is more than one.
return value1, value2 Every return statement must supply one value per declared return type, in the same order.
q, r := functionName(...) The caller receives every value in one multiple-assignment statement.

You can also name the return values in the signature, like func functionName(params) (name1 ReturnType1, name2 ReturnType2), which lets you assign to name1 and name2 inside the body and finish with a bare return.

Examples

Example 1: Returning a quotient and a remainder

The simplest case: a function that naturally produces two related numbers returns both, instead of forcing the caller to call it twice or pack the values into a struct.

package main

import "fmt"

func divmod(a, b int) (int, int) {
	return a / b, a % b
}

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

Output:

3 2

divmod declares two int return types, and its single return statement supplies both a / b and a % b, in that order. On the caller’s side, q, r := divmod(17, 5) assigns the first returned value to q and the second to r in one step.

Example 2: Named return values

Naming the return values documents their meaning directly in the signature and lets you use a bare return once they’ve been set.

package main

import "fmt"

func findRange(nums []int) (lo, hi int) {
	lo, hi = nums[0], nums[0]
	for _, n := range nums {
		if n < lo {
			lo = n
		}
		if n > hi {
			hi = n
		}
	}
	return
}

func main() {
	lo, hi := findRange([]int{4, 2, 9, -3, 7})
	fmt.Println(lo, hi)
}

Output:

-3 9

lo and hi are declared right in the signature as (lo, hi int) — Go zero-initializes them before the body runs, though here they’re immediately overwritten with nums[0]. Because they’re already declared, the loop assigns to them directly with =, and the bare return at the end sends back whatever lo and hi currently hold, equivalent to writing return lo, hi explicitly.

Example 3: Pairing a result with an error

The idiom you’ll see the most in real Go code: return a computed value together with an error, and let the caller decide what to do.

package main

import (
	"errors"
	"fmt"
)

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

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

	result2, err2 := safeDivide(5, 0)
	if err2 != nil {
		fmt.Println("Error:", err2)
	} else {
		fmt.Println("Result:", result2)
	}
}

Output:

Result: 5
Error: division by zero

safeDivide returns (float64, error). When b is zero it returns the zero value 0 for the float and a non-nil error describing the problem; otherwise it returns the real quotient and nil, meaning "no error occurred". The caller always checks err first — the numeric result is only meaningful when err is nil.

How It Works Step by Step

Trace what happens when the line q, r := divmod(17, 5) executes:

  1. Go evaluates the arguments 17 and 5 and calls divmod.
  2. Inside divmod, the return statement evaluates both expressions completely before anything is returned — first 17 / 5 (3), then 17 % 5 (2).
  3. Both values are handed back to the call site together, in the order they appear in the return statement.
  4. The caller’s := performs a multiple assignment: q receives the first value and r receives the second, as a single step — this is also why the classic a, b = b, a swap idiom works correctly without a temporary variable.
  5. If the caller only wants one of the values, it must still account for both — using the blank identifier for the one it discards: q, _ := divmod(17, 5).

There’s one special case worth knowing: if a function’s return values exactly match another function’s parameter list, you can pass the call straight through, as long as the multi-value call is the sole argument:

package main

import "fmt"

func divmod(a, b int) (int, int) {
	return a / b, a % b
}

func main() {
	fmt.Println(divmod(17, 5))
}

Output:

3 2

This works because fmt.Println accepts a variadic list of any values, and Go expands the two values from divmod directly into that list. You could not mix this with another argument, like fmt.Println(divmod(17, 5), "result") — that fails to compile.

Common Mistakes

1. Silently discarding the error

Wrong:

package main

import (
	"errors"
	"fmt"
)

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

func main() {
	result, _ := safeDivide(5, 0)
	fmt.Println("Result:", result)
}

Output:

Result: 0

This compiles fine, but throwing away err with _ destroys exactly the information you need: is 0 a real answer, or a failure that happened to produce the zero value? Always check the error instead:

package main

import (
	"errors"
	"fmt"
)

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

func main() {
	result, err := safeDivide(5, 0)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Result:", result)
}

Output:

Error: division by zero

2. Assigning a two-value return to one variable

Go’s compiler counts return values strictly. This will not compile:

func swap(a, b int) (int, int) {
	return b, a
}

func main() {
	x := swap(1, 2) // compile error: assignment mismatch: 1 variable but swap returns 2 values
	fmt.Println(x)
}

Every returned value needs a landing spot — a variable, or the blank identifier if you don’t need it:

x, y := swap(1, 2)
fmt.Println(x, y)

3. Shadowing a variable with := inside an if

Wrong:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	value, err := strconv.Atoi("42")
	if err != nil {
		fmt.Println("initial parse failed:", err)
	}

	if value, err := strconv.Atoi("oops"); err != nil {
		fmt.Println("second parse failed:", err, "got value:", value)
	}

	fmt.Println("value:", value)
}

Output:

second parse failed: strconv.Atoi: parsing "oops": invalid syntax got value: 0
value: 42

The if value, err := strconv.Atoi("oops"); err != nil line uses :=, which declares brand-new value and err variables scoped only to that if statement — it does not touch the outer ones. The final fmt.Println("value:", value) still prints the original 42, which is easy to miss in a quick read. Use plain = to reassign the existing variables instead:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	value, err := strconv.Atoi("42")
	if err != nil {
		fmt.Println("initial parse failed:", err)
	}

	value, err = strconv.Atoi("oops")
	if err != nil {
		fmt.Println("second parse failed:", err)
	}

	fmt.Println("value:", value)
}

Output:

second parse failed: strconv.Atoi: parsing "oops": invalid syntax
value: 0

Best Practices

  • Put the error last in the return list and conventionally name it err.
  • Check every error immediately after the call — don’t defer the check or discard it with _.
  • Prefer plain multiple return values for two or three simple results; switch to a small struct once you’re returning four or more values, or when they naturally belong together as one concept.
  • Use named return values to document meaning, especially when types alone are ambiguous (like two ints), but avoid relying on bare return in long or branching functions.
  • Use = instead of := when you want to update existing variables like value, err, so you don’t accidentally create a shadow copy inside a nested block.
  • Reach for the comma-ok idiom (v, ok := m[key]) instead of sentinel values like -1 or "" for "not found" — sentinels can collide with real data.
  • Keep return-type order consistent across your codebase (data first, error last) so callers can predict a function’s shape without checking docs.

Practice Exercises

  1. Write a function divide(a, b int) (int, int, error) that returns the quotient, the remainder, and a non-nil error when b is 0. Call it once with valid inputs and once with b = 0, printing either the quotient/remainder pair or the error message.
  2. Write stats(nums []int) (lo, hi, total int) using named return values that finds the minimum, maximum, and sum of a slice in a single loop, then finishes with a bare return. Test it on []int{5, -2, 8, 0, 3} — expected output: -2 8 14.
  3. Write lookup(scores map[string]int, name string) (int, bool) that wraps a map read and returns the score plus whether name was found, mirroring the comma-ok idiom. Call it once with a name that exists and once with one that doesn’t, and print both results.

Summary

  • A Go function returns multiple values by listing more than one type in parentheses after the parameter list.
  • The caller must consume every returned value in one multiple-assignment statement, using _ to ignore any it doesn’t need.
  • The (result, error) pairing is Go’s standard way of signaling failure without exceptions — always check err before trusting the result.
  • Named return values pre-declare variables you can assign to throughout the function body and send back with a bare return.
  • The comma-ok idiom (map lookups, type assertions, channel receives) reuses the same two-value shape outside of user-defined functions.
  • Watch for two classic bugs: silently discarding an error with _, and accidentally shadowing a variable by using := instead of = inside a nested if.