Go > Core Go Basics > Functions > Multiple return values

Returning Multiple Values in Go Functions

Go functions can return multiple values, offering a clean and efficient way to handle errors or return related data. This approach avoids the need for complex struct definitions when multiple return values are appropriate.

Basic Example: Dividing with Error Handling

This example demonstrates a simple division function that returns both the result and an error. If the divisor is zero, an error is returned. Otherwise, the result and a nil error are returned. The `errors.New()` function is used to create a new error instance with a descriptive message. In the `main` function, we check for the error before using the result.

package main

import (
	"fmt"
	"errors"
)

// divide function returns both the result and an error, if any.
func divide(dividend, divisor float64) (float64, error) {
	if divisor == 0 {
		return 0, errors.New("division by zero")
	}
	return dividend / divisor, nil
}

func main() {
	res, err := divide(10.0, 2.0)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Result:", res)

	res, err = divide(5.0, 0.0)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Result:", res)
}

Concepts Behind Multiple Return Values

Go's ability to return multiple values is a key feature for error handling and conveying related data points from a function. Instead of relying on exceptions (which Go doesn't have) or complex struct return types for simple scenarios, multiple return values provide a concise and readable approach. The last value returned is often an error type, allowing functions to gracefully handle and communicate failure conditions.

Real-Life Use Case: Reading Data with Status

Consider a scenario where you're fetching data from a database or an API. The function needs to return the data itself and also indicate whether the fetch was successful. Multiple return values (data, success) are a perfect fit here. The code example illustrates a `fetchData` function that returns a string and a boolean indicating success or failure.

package main

import (
	"fmt"
)

// fetchData simulates fetching data.  Returns data and a boolean indicating success.
func fetchData(id int) (string, bool) {
	if id < 0 {
		return "", false // Indicate failure
	}
	data := fmt.Sprintf("Data for ID %d", id)
	return data, true // Indicate success
}

func main() {
	data, ok := fetchData(123)
	if ok {
		fmt.Println("Data:", data)
	} else {
		fmt.Println("Failed to fetch data.")
	}

	data, ok = fetchData(-1)
	if ok {
		fmt.Println("Data:", data)
	} else {
		fmt.Println("Failed to fetch data.")
	}
}

Best Practices

  • Error Handling: Always check the error return value, especially when the function can potentially fail.
  • Named Return Values: For clarity, consider using named return values, especially when returning more than two values. This makes the function signature more readable.
  • Meaningful Error Messages: Provide informative error messages that help in debugging.
  • Don't Overuse: Avoid returning too many values. If you have more than 3-4 related pieces of data, consider using a struct.

Interview Tip

Be prepared to explain how Go handles errors (through multiple return values), why it's preferred over exceptions, and how it promotes explicit error handling. Also, be ready to demonstrate how to write a function with multiple return values and how to handle them in the calling code.

When to Use Them

Use multiple return values when you need to return related data or signal success/failure along with a result. They are particularly useful for error handling, returning status codes, or providing ancillary information along with the primary result of a function.

Alternatives

Alternatives to multiple return values include:

  • Returning a Struct: Useful when returning many related values that logically belong together. This can improve code organization.
  • Using Pointers: Functions can modify variables passed as pointers. However, this can lead to less readable code and potential side effects.

Pros

  • Clear Error Handling: Enforces explicit error handling.
  • Concise Syntax: Avoids complex exception handling mechanisms.
  • Improved Readability: Makes function signatures more descriptive.
  • Efficiency: Avoids the overhead of exception handling.

Cons

  • Can Be Verbose: Requires explicit error checking in the calling code.
  • Potential for Ignored Errors: Developers might forget to check the error return value. Tools like `errcheck` can help mitigate this.
  • Limited for Complex Data: Not ideal for returning a large number of unrelated values (use a struct instead).

FAQ

  • Can I ignore a return value?

    Yes, you can ignore a return value using the blank identifier `_`. For example: `_, err := someFunction()`. However, ignoring the error return value is generally discouraged, as it can lead to unexpected behavior.
  • What happens if a function doesn't return all the values it declares?

    Go will return the zero value for any missing return values. For example, if a function declares `(int, error)` but only explicitly returns an `error`, the `int` will be zero.