Skip to main content

Go Return Values

Introduction

In programming, functions often need to send data back to the code that called them. These outputs are called return values. Go has a particularly flexible and powerful approach to handling function return values, with capabilities that set it apart from many other programming languages.

In this guide, we'll explore how return values work in Go, from basic single value returns to more advanced concepts like named return values and multiple return patterns. By the end, you'll be comfortable working with Go's return value system and ready to use these concepts in your own programs.

Basic Return Values

At its simplest, a Go function can return a single value of any type.

Returning a Single Value

Let's start with a basic example - a function that adds two numbers and returns the result:

go
package main

import "fmt"

func add(x int, y int) int {
return x + y
}

func main() {
sum := add(5, 3)
fmt.Println("5 + 3 =", sum)
}

Output:

5 + 3 = 8

In this example:

  • The function signature func add(x int, y int) int indicates that add returns an integer.
  • The return keyword is followed by the value (or expression) we want to return.
  • When we call the function with sum := add(5, 3), the return value is stored in the sum variable.

Return Type Declaration

The return type is declared after the parameter list in the function declaration. Here are examples with different return types:

go
func getGreeting() string {
return "Hello, world!"
}

func isEven(num int) bool {
return num%2 == 0
}

func getPI() float64 {
return 3.14159
}

Multiple Return Values

One of Go's distinctive features is the ability for functions to return multiple values. This eliminates the need for out parameters or complex return objects found in other languages.

Returning Two or More Values

Here's a function that divides two numbers and returns both the quotient and remainder:

go
package main

import "fmt"

func divide(dividend, divisor int) (int, int) {
quotient := dividend / divisor
remainder := dividend % divisor
return quotient, remainder
}

func main() {
q, r := divide(13, 5)
fmt.Printf("13 ÷ 5 = %d with remainder %d
", q, r)
}

Output:

13 ÷ 5 = 2 with remainder 3

In this example:

  • The function signature func divide(dividend, divisor int) (int, int) shows that the function returns two integer values.
  • The values are returned as a comma-separated list: return quotient, remainder.
  • When calling the function, we use multiple variables to capture the return values: q, r := divide(13, 5).

Ignoring Return Values

If you don't need all return values, you can use the blank identifier (_) to ignore specific ones:

go
package main

import "fmt"

func getFullName() (string, string) {
return "John", "Doe"
}

func main() {
// Only care about the first name
firstName, _ := getFullName()
fmt.Println("First name:", firstName)

// Only care about the last name
_, lastName := getFullName()
fmt.Println("Last name:", lastName)
}

Output:

First name: John
Last name: Doe

Named Return Values

Go allows you to name your return values in the function declaration. Named return values are initialized as zero values at the beginning of the function and can be returned using a "naked" return statement.

Basic Named Returns

go
package main

import "fmt"

func divide(dividend, divisor int) (quotient int, remainder int) {
quotient = dividend / divisor
remainder = dividend % divisor
return // This is a "naked" return
}

func main() {
q, r := divide(13, 5)
fmt.Printf("13 ÷ 5 = %d with remainder %d
", q, r)
}

Output:

13 ÷ 5 = 2 with remainder 3

In this version:

  • We've named the return values quotient and remainder in the function signature.
  • These variables are automatically declared and initialized to zero at the function's start.
  • We assign values to them during function execution.
  • The return statement without arguments returns the current values of the named return variables.

Combining Named Returns with Explicit Returns

You can still use explicit returns with named return values:

go
func divide(dividend, divisor int) (quotient int, remainder int) {
quotient = dividend / divisor
remainder = dividend % divisor
return quotient, remainder // This is equivalent to the naked return above
}

Benefits of Named Returns

Named returns offer several advantages:

  1. Self-documenting code: The names indicate what each return value represents.
  2. Easier to understand: Complex functions with multiple returns become more readable.
  3. Default initialization: Named return variables are initialized to their zero values.

Common Return Patterns in Go

Go has established several idiomatic patterns for function returns.

The Error Return Pattern

One of the most common patterns in Go is returning a result along with an error:

go
package main

import (
"fmt"
"errors"
)

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.Println("10 ÷ 2 =", result)
}

// Error case
result, err = divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("10 ÷ 0 =", result)
}
}

Output:

10 ÷ 2 = 5
Error: division by zero

This pattern:

  • Returns the result and an error value (nil if no error occurred)
  • Allows the caller to check if an error occurred before using the result
  • Is idiomatic Go and extensively used in the standard library

The Boolean Success Pattern

For functions that might succeed or fail but don't need error details:

go
package main

import "fmt"

func findElement(slice []int, target int) (int, bool) {
for i, value := range slice {
if value == target {
return i, true // Found, return index and true
}
}
return -1, false // Not found, return -1 and false
}

func main() {
numbers := []int{10, 20, 30, 40, 50}

if index, found := findElement(numbers, 30); found {
fmt.Printf("Found 30 at index %d
", index)
} else {
fmt.Println("30 not found")
}

if index, found := findElement(numbers, 35); found {
fmt.Printf("Found 35 at index %d
", index)
} else {
fmt.Println("35 not found")
}
}

Output:

Found 30 at index 2
35 not found

Best Practices for Return Values

To make your Go code more idiomatic and maintainable, consider these best practices:

1. Be Consistent with Return Types

If multiple functions serve similar purposes, try to maintain consistent return signatures:

go
// Bad: Inconsistent return patterns
func findUserByID(id int) User
func findUserByEmail(email string) (User, error)

// Good: Consistent return patterns
func findUserByID(id int) (User, error)
func findUserByEmail(email string) (User, error)

2. Use Named Returns for Clarity, Not Convenience

Named returns can make your code more readable, but don't overuse them:

go
// Good use of named returns - clear what each value represents
func splitName(fullName string) (first, middle, last string, err error) {
// Implementation
}

// Less helpful - doesn't add much clarity
func add(a, b int) (result int) {
result = a + b
return
}

3. Return Early for Error Cases

To keep code readable, handle errors at the beginning of your function:

go
func processFile(filename string) ([]byte, error) {
// Return early on error
if filename == "" {
return nil, errors.New("empty filename")
}

file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

// Continue with normal processing
return ioutil.ReadAll(file)
}

Real-World Applications

Let's explore some practical examples of how return values are used in real Go programs.

Example 1: Parsing Configuration Values

go
package main

import (
"fmt"
"strconv"
)

// ParseConfig takes a map of string configuration values and extracts typed values
func ParseConfig(config map[string]string) (port int, debug bool, prefix string, err error) {
// Set default values
port = 8080
prefix = "app"

// Parse port
if portStr, exists := config["port"]; exists {
port, err = strconv.Atoi(portStr)
if err != nil {
return 0, false, "", fmt.Errorf("invalid port: %w", err)
}
}

// Parse debug flag
if debugStr, exists := config["debug"]; exists {
debug, err = strconv.ParseBool(debugStr)
if err != nil {
return 0, false, "", fmt.Errorf("invalid debug setting: %w", err)
}
}

// Parse prefix
if p, exists := config["prefix"]; exists {
prefix = p
}

return port, debug, prefix, nil
}

func main() {
// Sample configuration
config := map[string]string{
"port": "9090",
"debug": "true",
"prefix": "myapp",
}

port, debug, prefix, err := ParseConfig(config)
if err != nil {
fmt.Println("Configuration error:", err)
return
}

fmt.Printf("Starting %s on port %d (debug: %t)
", prefix, port, debug)
}

Output:

Starting myapp on port 9090 (debug: true)

Example 2: Database Operations

Here's how you might use return values in database operations:

go
package main

import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)

type User struct {
ID int
Name string
Email string
}

// GetUser retrieves a user by ID from the database
func GetUser(db *sql.DB, id int) (User, bool, error) {
var user User

row := db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id)
err := row.Scan(&user.ID, &user.Name, &user.Email)

if err == sql.ErrNoRows {
return User{}, false, nil // User not found, but not an error
} else if err != nil {
return User{}, false, err // Database error
}

return user, true, nil // User found
}

// CreateUser adds a new user to the database
func CreateUser(db *sql.DB, name, email string) (int, error) {
result, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", name, email)
if err != nil {
return 0, err
}

// Get the ID of the newly inserted user
id, err := result.LastInsertId()
if err != nil {
return 0, err
}

return int(id), nil
}

// Example usage (not runnable without actual DB)
func ExampleUsage() {
// Open database connection
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
fmt.Println("Error opening database:", err)
return
}
defer db.Close()

// Create tables and sample data
db.Exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`)

// Create a user
newID, err := CreateUser(db, "Alice Smith", "[email protected]")
if err != nil {
fmt.Println("Error creating user:", err)
return
}
fmt.Printf("Created user with ID: %d
", newID)

// Retrieve the user
user, found, err := GetUser(db, newID)
if err != nil {
fmt.Println("Database error:", err)
} else if !found {
fmt.Println("User not found")
} else {
fmt.Printf("Found user: ID=%d, Name=%s, Email=%s
",
user.ID, user.Name, user.Email)
}
}

This example demonstrates:

  1. Multiple return types (user, found flag, error)
  2. Different return patterns for create and get operations
  3. Handling "not found" as a normal condition, not an error

Summary

Go's return value system offers significant flexibility and power:

  • Single returns provide basic function output
  • Multiple returns allow functions to return several values without complex structures
  • Named returns improve code clarity and reduce repetition
  • Common patterns like error returns provide consistent error handling

By understanding these concepts, you can write more idiomatic Go code that's both robust and easy to understand.

Exercises

To practice working with Go return values, try these exercises:

  1. Create a function that calculates the area and perimeter of a rectangle, returning both values.
  2. Write a function that searches a string for a substring and returns the position and a boolean indicating if it was found.
  3. Implement a function that parses a time string in the format "HH:MM:SS" and returns the hours, minutes, seconds, and an error if the format is invalid.
  4. Create a division function that handles division by zero using the error return pattern.
  5. Write a function with named return values that converts a temperature from Celsius to both Fahrenheit and Kelvin.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)