The contract has multiple outputs
You're building a function to split a batch of tasks among workers. You divide 100 tasks by 7 workers. You need to know how many tasks each worker gets, and you also need to know how many tasks are left over to assign manually. In JavaScript, you'd return an object like { quotient: 14, remainder: 2 }. In Python, you'd use a tuple or divmod. In C, you'd pass a pointer for the result and return the remainder as the function's return value.
Go gives you a simpler tool. Functions return exactly what you ask for, and you can ask for more than one thing. The function signature lists every output. The caller captures every output. The compiler enforces the match. You don't need a wrapper struct for every pair of values. You don't need to check a magic error code. The data flow is explicit in the signature.
Think of a function signature as a contract. The inputs are what you hand over. The outputs are what you get back. Most languages let you hand back one box. Go lets you hand back multiple items directly. It's like a waiter placing a plate and a drink on the table at the same time, rather than wrapping them in a single tray. The compiler handles the unpacking. You don't need a wrapper type unless you have a good reason.
How the compiler handles the unpacking
When you write a function with multiple return values, the compiler generates code to manage all of them. There is no hidden allocation. There is no garbage collection pressure. The overhead is identical to returning a single value.
Here's the simplest case: a function that returns a quotient and a remainder.
// Divide returns the quotient and remainder of a division.
func Divide(a, b int) (int, int) {
// Return two values separated by a comma.
return a / b, a % b
}
func main() {
// Capture both values into named variables.
quotient, remainder := Divide(10, 3)
// quotient is 3, remainder is 1.
}
When Divide executes, the CPU calculates a / b and a % b. These results land in return registers. The caller's stack frame has space reserved for two integers. The return instruction jumps back, and the values are already in place. The assignment quotient, remainder := Divide(10, 3) just names the slots. The compiler knows to grab the first result for quotient and the second for remainder.
If you have a function returning five values, the compiler might spill some to the stack, but the logic is the same. The performance cost is zero compared to a single return. You gain clarity without paying in speed. The syntax makes the data flow visible. Anyone reading the call site sees exactly what comes back.
Real code: errors and flags
Real code uses multiple returns for control flow and error reporting. The standard library relies on this pattern everywhere. The most common case is returning a result alongside an error.
Here's a function that looks up a user and returns the user struct plus an error.
// LookupUser finds a user by ID and returns the user and an error.
// If the user is not found, the error is non-nil and the user is nil.
func LookupUser(id int) (*User, error) {
// Simulate a lookup.
if id == 42 {
// Return a valid user and a nil error.
return &User{Name: "Alice"}, nil
}
// Return nil and a descriptive error.
return nil, fmt.Errorf("user %d not found", id)
}
func main() {
// Call the function and capture both results.
user, err := LookupUser(42)
// Check the error first. This is the standard Go idiom.
if err != nil {
// Handle the error case.
log.Fatal(err)
}
// Use the user safely.
fmt.Println(user.Name)
}
The pattern if err != nil is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. You can't accidentally ignore the error. The check sits right next to the call. This convention pays off in large codebases where silent failures are expensive.
Not all second returns are errors. Sometimes you need a flag or a secondary result. Here's a search function that returns an index and a boolean.
// Search finds the index of a value in a slice.
// It returns the index and a boolean indicating if the value was found.
func Search(slice []int, target int) (int, bool) {
// Iterate over the slice to find the target.
for i, v := range slice {
if v == target {
// Return the index and true.
return i, true
}
}
// Return -1 and false if not found.
return -1, false
}
func main() {
// Call Search and capture both results.
idx, found := Search([]int{1, 2, 3}, 2)
// Check the flag before using the index.
if found {
fmt.Println("Found at", idx)
}
}
The boolean flag tells the caller whether the index is valid. This avoids returning a sentinel value like -1 that might be a valid index in some contexts. The signature documents the behavior. The caller handles both cases.
Pitfalls and compiler traps
Multiple returns are safe, but they have traps. The compiler catches most mistakes, but some patterns lead to subtle bugs.
If you call a function that returns two values but only assign one, the compiler stops you. You get assignment mismatch: 1 variable but f returns 2 values. Go forces you to acknowledge every return value. If you don't want one, use the blank identifier _. This tells the compiler "I know this exists, but I'm discarding it."
func main() {
// Discard the remainder. The underscore consumes the value.
quotient, _ := Divide(10, 3)
// quotient is 3. The remainder is dropped.
}
Use _ sparingly with errors. Discarding an error without a comment is suspicious. If you ignore an error, add a comment explaining why. The compiler won't warn you, but your future self will thank you.
Named return values are another trap. You can name the return variables in the signature. This makes the signature self-documenting, but it introduces risk.
// BadNamedReturn shows the risk of bare returns.
func BadNamedReturn(x int) (result int, err error) {
if x < 0 {
// err is set, but result is zero.
err = fmt.Errorf("negative")
// Bare return uses current values.
return
}
// result is set.
result = x * 2
return
}
Named returns let you use a bare return without values. The compiler fills in the current values of the named variables. This is convenient for short functions. It's dangerous for long functions with multiple return paths. You might forget to set a variable before returning. The compiler doesn't catch uninitialized named returns if you use bare return. It's better to avoid named returns in complex functions. Explicit returns are safer.
The compiler also rejects mismatched types. If a function returns (int, error), you can't return (string, error). You get cannot use x (untyped int constant) as string value in argument if you pass the wrong type. The types must match exactly.
Choosing the right return shape
Multiple returns are a tool, not a rule. You need to pick the right shape for your function.
Use multiple return values when you have a small, fixed set of related results, like a value and an error.
Use multiple return values when you need to return a result alongside metadata, such as a boolean flag or a secondary count.
Use a struct when you have three or more return values, or when the return data has semantic meaning as a single entity.
Use a struct when the return data has fields that should be named and documented together.
Use a single return value when the function computes a pure transformation with no side effects or status codes.
Use a slice when the number of results is dynamic, or when the results are a collection of items.
Use an interface return when you want to hide implementation details and allow the caller to depend only on methods.
Multiple returns are cheap. Structs are not free. Pick the shape that matches the data. Don't force a struct for two values. Don't force multiple returns for five values. The signature should tell the story.