The vending machine with two slots
You are writing a function to parse a user ID from a request string. The parsing works, but the string might be malformed. In Python, you might raise an exception. In JavaScript, you might return an object with an error property. Go gives you a different tool: the function returns the ID and the error side by side. No hidden wrappers. No exception machinery. Just values.
Multiple returns are a first-class feature of Go. Functions can return any number of values. The language treats these returns as a tuple, making them cheap to allocate and easy to inspect. This design shapes how Go code handles errors, status flags, and related data. You will see multiple returns everywhere in the standard library and in production code.
Think of a function like a vending machine with two slots. One slot drops the product. The other slot drops the receipt. You always check the receipt to see if the transaction succeeded before you use the product. The machine does not hide the receipt inside the product wrapper. Both items come out together, and the caller decides what to do with each.
How the signature works
A Go function declares its return values in the signature, right after the parameters. You list the types separated by commas. The return statement provides the values in the same order.
Here is the simplest pattern: a function returns a result and a boolean flag.
package main
import "fmt"
// parseID extracts an integer from a string and reports success.
// The function returns the ID and a boolean flag.
func parseID(input string) (int, bool) {
// convert the string to an integer
id, err := fmt.Atoi(input)
if err != nil {
// return zero and false if parsing fails
return 0, false
}
// return the valid ID and true
return id, true
}
func main() {
// capture both return values
id, ok := parseID("42")
if ok {
fmt.Println("ID:", id)
}
}
The compiler enforces the order and count. If you return too few values, the build fails with not enough arguments in return to return 2 values. If you return too many, you get too many arguments in return. If the types do not match, the compiler rejects the code with cannot use x (type int) as type string in return argument.
The caller must capture every return value unless it explicitly discards one. You cannot call parseID and ignore the second value without using the blank identifier. The compiler complains with assignment mismatch: 1 variable but parseID returns 2 values if you try to assign the result to a single variable.
Under the hood: tuples and stack space
Go does not create a hidden struct for multiple return values. The compiler generates code that treats the returns as a tuple. When you call the function, the caller reserves space for the return values. In most cases, this space lives on the stack. The function writes directly into that space.
This mechanism is efficient. There is no heap allocation for the return wrapper. There is no garbage collection pressure from the return mechanism itself. You can return a result, an error, and a metadata flag without worrying about performance overhead. The cost is comparable to returning a single value.
The efficiency encourages developers to return multiple values freely. You do not need to bundle results into a struct just to avoid allocation. If a function naturally produces two or three related values, return them directly. The compiler handles the plumbing.
Named returns: the feature that bites
Go allows you to name the return values in the signature. The names act as local variables initialized to their zero values. You can assign to them inside the function and use a bare return to send them back.
// stats calculates the minimum and maximum of a slice.
// The return values are named min and max.
func stats(data []int) (min int, max int) {
// initialize min and max with the first element
min = data[0]
max = data[0]
// iterate over the rest of the slice
for _, v := range data[1:] {
if v < min {
min = v
}
if v > max {
max = v
}
}
// bare return sends the current values of min and max
return
}
Named returns look clean. They document what each value represents. They allow a bare return that can reduce boilerplate. But they introduce a risk. If you have multiple return paths and forget to set a value, the function returns zero. The compiler will not stop you. The bug hides in production.
Consider a function that returns a user and an error. If you name the returns user User and err error, and you forget to set err on a success path, the function returns a zero-value user and a nil error. The caller thinks everything worked. The bug is silent.
Most Go teams avoid named returns in complex functions. They are acceptable in benchmarks where you return timing data, or in trivial getters where the logic is a single line. For everything else, unnamed returns force you to write the values out, which makes the return path explicit. The extra keystrokes buy you safety.
Named returns save keystrokes but cost clarity. Prefer unnamed returns.
The error convention
Error handling in Go relies on multiple returns. The convention is strict: the error value is always the last return value. Functions that can fail return (result, error). Functions that always succeed return just the result.
// getUser fetches a user by ID.
// It returns the user and an error.
func getUser(id int) (User, error) {
// simulate a database lookup
if id <= 0 {
// return zero value and an error for invalid ID
return User{}, fmt.Errorf("invalid ID: %d", id)
}
// return the user and nil error on success
return User{ID: id, Name: "Alice"}, nil
}
func main() {
// call the function and capture both values
user, err := getUser(1)
if err != nil {
// handle the error before using the user
fmt.Println("Error:", err)
return
}
// use the user only if error is nil
fmt.Println("User:", user.Name)
}
The community accepts the verbosity of if err != nil. It makes the unhappy path visible. You cannot ignore an error without explicitly discarding it with _. This design choice prevents silent failures. If you write user, _ := getUser(1), you are telling the compiler and future readers that you considered the error and chose to drop it. Use this sparingly. Discarding an error is a decision that should be rare and justified.
The err variable name is a convention. Functions that return an error should name it err. Callers should check if err != nil. This pattern is so common that tools and linters enforce it. You will see it in every Go codebase.
Errors are values. Handle them or propagate them.
Real world: parsing with safety
Multiple returns shine when you need to return a result and a status that are logically distinct. Parsing is a classic example. You want the parsed value, but you also need to know if the input was valid.
Here is a realistic example that parses a configuration key-value pair. The function returns the key, the value, and an error.
// parseKeyValue splits a string into key and value.
// It returns the key, value, and an error.
func parseKeyValue(input string) (string, string, error) {
// find the separator
idx := strings.Index(input, "=")
if idx == -1 {
// return empty strings and an error if separator is missing
return "", "", fmt.Errorf("missing separator in %q", input)
}
// extract key and value
key := strings.TrimSpace(input[:idx])
value := strings.TrimSpace(input[idx+1:])
// return the key, value, and nil error
return key, value, nil
}
func main() {
// parse a valid input
k, v, err := parseKeyValue("host=localhost")
if err != nil {
fmt.Println("Parse error:", err)
return
}
fmt.Println("Key:", k, "Value:", v)
// parse an invalid input and discard the values
_, _, err = parseKeyValue("invalid")
if err != nil {
fmt.Println("Expected error:", err)
}
}
The caller checks the error first. If the error is nil, the key and value are valid. If the error is not nil, the key and value are zero values and should not be used. The underscore discards values when you only care about the error. This pattern keeps the code readable and safe.
The underscore is your friend when you only care about half the result.
Pitfalls and compiler errors
Multiple returns are simple, but they have traps. Order matters. The return statement must provide values in the same order as the signature. If you swap them, the compiler might catch a type mismatch, or worse, the code compiles but the logic is wrong.
If you forget to return all values, the compiler rejects the program with not enough arguments in return. If you try to return a value of the wrong type, you get cannot use x (type int) as type string in return argument. These errors are clear and actionable.
Named returns can cause subtle bugs. A bare return uses the current values of the named variables. If you have a deferred function that modifies a named return value, the modification happens before the return. This can be useful, but it can also confuse readers.
func risky() (result int) {
// defer runs before the return
defer func() {
// modify the named return value
result += 10
}()
// return 5, but defer changes it to 15
return 5
}
This pattern is advanced. Most code should avoid modifying named returns in deferred functions. Stick to unnamed returns and explicit return statements for clarity.
When to use what
Go gives you options for returning data. Choose the right tool based on the number of values, the cohesion of the data, and the error handling needs.
Use multiple returns when you need to return a result and an error, or a small set of related values that are always used together.
Use a struct when you have three or more return values, or when the values form a cohesive entity that might be passed around as a unit.
Use an error wrapper when you need to attach context to an error without cluttering the return signature with extra fields.
Use a single return when the function has no failure mode and produces exactly one output.
Multiple returns are the default for error handling. They are explicit, efficient, and idiomatic. Structs are better for complex data. Error wrappers are better for context. Pick the pattern that matches the shape of your data.