Go Multiple Returns
Introduction
One of Go's most powerful and distinctive features is the ability for functions to return multiple values. Unlike many traditional programming languages that restrict functions to returning a single value, Go allows functions to return several values at once. This feature simplifies error handling, eliminates the need for reference parameters, and makes your code more readable and maintainable.
In this tutorial, we'll explore how multiple return values work in Go, common patterns for using them, and practical examples to help you incorporate this feature into your own programs.
Basic Syntax for Multiple Returns
In Go, a function can return multiple values by listing all return types in parentheses in the function declaration.
func functionName(parameters) (returnType1, returnType2, ...) {
// function body
return value1, value2, ...
}
Let's look at a simple example:
package main
import "fmt"
func getNameAndAge() (string, int) {
return "John", 30
}
func main() {
name, age := getNameAndAge()
fmt.Printf("Name: %s, Age: %d
", name, age)
}
Output:
Name: John, Age: 30
In this example, the getNameAndAge()
function returns two values: a string and an integer. When we call this function, we use multiple assignment to capture both return values in separate variables.
Named Return Values
Go also supports named return values. When you declare a function with named return values, those variables are initialized with their zero values and can be used within the function body. A simple return
statement without arguments will return the current values of those variables.
func rectangleProperties(length, width float64) (area, perimeter float64) {
area = length * width
perimeter = 2 * (length + width)
return // returns the named variables: area and perimeter
}
Here's a complete example:
package main
import "fmt"
func rectangleProperties(length, width float64) (area, perimeter float64) {
area = length * width
perimeter = 2 * (length + width)
return
}
func main() {
area, perimeter := rectangleProperties(5.0, 3.0)
fmt.Printf("Area: %.2f, Perimeter: %.2f
", area, perimeter)
}
Output:
Area: 15.00, Perimeter: 16.00
Named return values make your code more readable and self-documenting, as the purpose of each return value is clear from the function declaration.
Ignoring Return Values
Sometimes you might need only some of the return values from a function. Go allows you to ignore unwanted return values by using the blank identifier _
.
package main
import "fmt"
func getCoordinates() (int, int, int) {
return 10, 20, 30
}
func main() {
// Only interested in x and z coordinates
x, _, z := getCoordinates()
fmt.Printf("X: %d, Z: %d
", x, z)
}
Output:
X: 10, Z: 30
In this example, we're ignoring the y-coordinate by using _
in the assignment.
Error Handling with Multiple Returns
One of the most common uses of multiple return values in Go is for error handling. By convention, the last return value is used to indicate whether the function executed successfully or encountered 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() {
// Successful case
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("10 / 2 = %.2f
", result)
}
// Error case
result, err = divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("10 / 0 = %.2f
", result)
}
}
Output:
10 / 2 = 5.00
Error: division by zero
This pattern is idiomatic in Go. It allows for clear and straightforward error handling without exceptions or special return codes.
Practical Examples
Example 1: Parsing User Input
package main
import (
"fmt"
"strconv"
"strings"
)
func parseUserInfo(input string) (string, int, error) {
parts := strings.Split(input, ",")
if len(parts) != 2 {
return "", 0, fmt.Errorf("invalid input format: expected 'name,age'")
}
name := strings.TrimSpace(parts[0])
if name == "" {
return "", 0, fmt.Errorf("name cannot be empty")
}
ageStr := strings.TrimSpace(parts[1])
age, err := strconv.Atoi(ageStr)
if err != nil {
return "", 0, fmt.Errorf("invalid age: %v", err)
}
if age < 0 || age > 150 {
return "", 0, fmt.Errorf("age out of reasonable range (0-150)")
}
return name, age, nil
}
func main() {
inputs := []string{
"Alice, 30",
"Bob, abc",
"Charlie",
", 25",
"Diana, 200",
}
for _, input := range inputs {
name, age, err := parseUserInfo(input)
if err != nil {
fmt.Printf("Error parsing '%s': %v
", input, err)
} else {
fmt.Printf("Successfully parsed: Name = %s, Age = %d
", name, age)
}
}
}
Output:
Successfully parsed: Name = Alice, Age = 30
Error parsing 'Bob, abc': invalid age: strconv.Atoi: parsing "abc": invalid syntax
Error parsing 'Charlie': invalid input format: expected 'name,age'
Error parsing ', 25': name cannot be empty
Error parsing 'Diana, 200': age out of reasonable range (0-150)
This example demonstrates how multiple returns can help validate and parse user input while providing meaningful error messages.
Example 2: File Operations
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
func countFileStats(filename string) (int, int, int, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return 0, 0, 0, fmt.Errorf("error reading file: %v", err)
}
content := string(data)
lines := strings.Split(content, "
")
lineCount := len(lines)
wordCount := 0
charCount := len(content)
for _, line := range lines {
words := strings.Fields(line)
wordCount += len(words)
}
return lineCount, wordCount, charCount, nil
}
func main() {
// Create a temporary file for demonstration
tempFile, err := ioutil.TempFile("", "example")
if err != nil {
fmt.Println("Error creating temp file:", err)
return
}
defer os.Remove(tempFile.Name())
content := "Hello world!
This is an example.
Multiple returns in Go are powerful."
if _, err := tempFile.WriteString(content); err != nil {
fmt.Println("Error writing to file:", err)
return
}
if err := tempFile.Close(); err != nil {
fmt.Println("Error closing file:", err)
return
}
// Count file statistics
lines, words, chars, err := countFileStats(tempFile.Name())
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("File stats: %d lines, %d words, %d characters
", lines, words, chars)
}
}
Output:
File stats: 3 lines, 12 words, 69 characters
This example shows how multiple returns can be used to return several statistics about a file in a single function call.
Common Patterns and Best Practices
1. Result and Error Pattern
The most common pattern in Go is returning a result and an error. This is used extensively in the standard library:
result, err := someFunction()
if err != nil {
// handle error
return
}
// use result
2. Found and Value Pattern
When searching for something that might not exist, a common pattern is to return whether the item was found and the item itself:
func findUser(id int) (user User, found bool) {
// Search for user
if userExists {
return foundUser, true
}
return User{}, false
}
Usage:
if user, found := findUser(123); found {
// Use user
} else {
// Handle not found case
}
3. Keep Return Types Simple
While you can return many values, it's usually best to keep the number manageable. If you need to return more than 3-4 values, consider using a struct instead:
// Instead of:
func complexOperation() (int, string, bool, float64, error) {
// ...
}
// Consider:
type OperationResult struct {
Count int
Name string
IsValid bool
Score float64
}
func complexOperation() (OperationResult, error) {
// ...
}
4. Consistent Order for Similar Functions
When you have multiple functions with similar purposes, keep the order of return values consistent:
// Good - consistent order
func findUserByID(id int) (User, error)
func findUserByEmail(email string) (User, error)
// Bad - inconsistent order
func findUserByID(id int) (User, error)
func findUserByEmail(email string) (error, User) // Inconsistent!
Comparison with Other Languages
Many languages have different approaches to returning multiple values:
Go's approach is notable for its simplicity and clarity. You don't need to create temporary containers or use output parameters—multiple returns are a first-class language feature.
Summary
Go's multiple return values feature provides several benefits:
- Simplified error handling: The result-and-error pattern eliminates the need for exceptions or special error codes.
- Improved readability: Functions can return related values directly without the need for wrapper objects.
- Better API design: Functions can return natural combinations of values without artificial containers.
- Reduced boilerplate: No need for "out" parameters or container classes like in some other languages.
When working with multiple returns, remember these key points:
- Use named returns for self-documenting code when appropriate
- The blank identifier (
_
) can ignore unwanted return values - By convention, errors are the last return value
- Consider using a struct for functions that would otherwise return many values
Exercises
-
Write a function called
minMax
that takes a slice of integers and returns both the minimum and maximum values in the slice. -
Create a function called
parseTime
that accepts a string in the format "hh:mm" and returns the corresponding hours and minutes as integers, along with an error if the format is invalid. -
Implement a function called
divide
that takes two integers and returns the quotient, remainder, and a boolean indicating whether the division was exact (remainder is zero). -
Write a function called
fileInfo
that accepts a filename and returns the file size, creation time, and an error if the file cannot be accessed. -
Create a
validateUser
function that takes a username and password and returns a boolean indicating if the credentials are valid, a user ID if valid, and an appropriate error message if invalid.
Additional Resources
- Go Tour: Multiple Results
- Effective Go: Multiple Returns
- Go By Example: Multiple Return Values
- Go Standard Library: Many examples of multiple return values in the standard library
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)