Go Programming Series: This lesson is part of our comprehensive Complete Go Tutorial: Learn Go (Golang) from Beginner to Advanced.
Multiple Return Values in Go: Patterns, Errors and Best Practices
Multiple return values are a defining feature of Go APIs. They allow a function to return related outputs without creating temporary wrapper objects and, most importantly, make result-and-error handling explicit.
What You Will Learn
- Multiple result syntax
- Result plus error
- Zero values on failure
- Blank identifier
- Returning related values
- Error propagation
- Error context
- Named results
- When to return a struct
- Testing multiple results
1. Core Concept and Mental Model
A function can return one value, several values, or no values. The signature tells callers exactly what they receive.
The result-plus-error pattern is idiomatic Go: callers inspect the error before relying on a result whose validity depends on success.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
2. Basic Syntax and Structure
The result-plus-error pattern is idiomatic Go: callers inspect the error before relying on a result whose validity depends on success.
When an operation fails, the non-error results should have documented semantics; often they are their zero values.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
func load(id string) (Record, error) { /* lookup */ }
record, err := load("42")
if err != nil { return err }
3. How Go Handles It
When an operation fails, the non-error results should have documented semantics; often they are their zero values.
The blank identifier explicitly discards a result. It should not be used merely to hide an error that should have been handled.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
4. Practical Example
The blank identifier explicitly discards a result. It should not be used merely to hide an error that should have been handled.
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
func load(id string) (Record, error) { /* lookup */ }
record, err := load("42")
if err != nil { return err }
5. Production Design Considerations
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
6. Common Variations
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
Error wrapping can add operation-specific context while preserving the underlying cause for callers that need to inspect it.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
func load(id string) (Record, error) { /* lookup */ }
record, err := load("42")
if err != nil { return err }
7. Edge Cases and Boundaries
Error wrapping can add operation-specific context while preserving the underlying cause for callers that need to inspect it.
Named results can clarify certain APIs but can also make control flow harder to follow when combined with naked returns.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
8. Readability and Maintainability
Named results can clarify certain APIs but can also make control flow harder to follow when combined with naked returns.
Use a struct when named fields improve readability, especially when the result is likely to grow over time.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
9. Testing Strategy
Use a struct when named fields improve readability, especially when the result is likely to grow over time.
Tests should validate both successful values and error states, including malformed, boundary and partial-result cases.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
func load(id string) (Record, error) { /* lookup */ }
record, err := load("42")
if err != nil { return err }
10. Error Handling and Failure Modes
Tests should validate both successful values and error states, including malformed, boundary and partial-result cases.
A function can return one value, several values, or no values. The signature tells callers exactly what they receive.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
11. Performance Considerations
A function can return one value, several values, or no values. The signature tells callers exactly what they receive.
The result-plus-error pattern is idiomatic Go: callers inspect the error before relying on a result whose validity depends on success.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
12. API and Architecture
The result-plus-error pattern is idiomatic Go: callers inspect the error before relying on a result whose validity depends on success.
When an operation fails, the non-error results should have documented semantics; often they are their zero values.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
func load(id string) (Record, error) { /* lookup */ }
record, err := load("42")
if err != nil { return err }
13. Common Mistakes
When an operation fails, the non-error results should have documented semantics; often they are their zero values.
The blank identifier explicitly discards a result. It should not be used merely to hide an error that should have been handled.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
14. Practical Workflow
The blank identifier explicitly discards a result. It should not be used merely to hide an error that should have been handled.
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
15. Frequently Asked Questions
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
16. Professional Checklist
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
Error wrapping can add operation-specific context while preserving the underlying cause for callers that need to inspect it.
In practice, the surrounding package and callers matter as much as the local syntax. A strong implementation makes its contract visible, keeps state ownership understandable, and leaves enough room for future maintenance without exposing unnecessary complexity.
At a Glance
| Concept | Typical Use | Main Consideration |
|---|---|---|
| Simple case | Use the direct language construct | Prefer clarity |
| Reusable API | Expose a focused contract | Limit coupling |
| Production code | Combine with tests and tooling | Make behavior explicit |
| Advanced design | Use only when justified | Document invariants |
Frequently Asked Questions
Why does Go return errors as values?
It keeps failure explicit in the function contract and lets the caller decide how to respond.
Can a function return many values?
Yes, but a struct may be clearer when the outputs need named fields.
Should errors be last?
Idiomatic Go generally places error as the final result.
Conclusion
The most useful way to learn this topic is to connect syntax with design. In professional Go code, language features are valuable when they produce clear contracts, predictable behavior, testable components and maintainable package boundaries.
Use the examples as starting points, then apply the same reasoning to real services, command-line tools and libraries. The goal is not to make code clever; it is to make the intended behavior obvious to the next developer.
Further Practical Considerations
A function can return one value, several values, or no values. The signature tells callers exactly what they receive.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
The result-plus-error pattern is idiomatic Go: callers inspect the error before relying on a result whose validity depends on success.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
When an operation fails, the non-error results should have documented semantics; often they are their zero values.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
The blank identifier explicitly discards a result. It should not be used merely to hide an error that should have been handled.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
Error wrapping can add operation-specific context while preserving the underlying cause for callers that need to inspect it.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
Named results can clarify certain APIs but can also make control flow harder to follow when combined with naked returns.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
Use a struct when named fields improve readability, especially when the result is likely to grow over time.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
Tests should validate both successful values and error states, including malformed, boundary and partial-result cases.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
A function can return one value, several values, or no values. The signature tells callers exactly what they receive.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
The result-plus-error pattern is idiomatic Go: callers inspect the error before relying on a result whose validity depends on success.
Experienced teams usually validate this behavior with focused tests, code review and normal Go tooling. Keeping the rule explicit is more valuable than relying on tribal knowledge.
Final Takeaway
The strongest implementation is the one that remains easy to understand when the codebase grows. Keep the feature focused, make contracts explicit, and let standard Go conventions reduce unnecessary complexity.
Advanced Review Notes
When an operation fails, the non-error results should have documented semantics; often they are their zero values.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
The blank identifier explicitly discards a result. It should not be used merely to hide an error that should have been handled.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Error wrapping can add operation-specific context while preserving the underlying cause for callers that need to inspect it.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Named results can clarify certain APIs but can also make control flow harder to follow when combined with naked returns.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Use a struct when named fields improve readability, especially when the result is likely to grow over time.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Tests should validate both successful values and error states, including malformed, boundary and partial-result cases.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
A function can return one value, several values, or no values. The signature tells callers exactly what they receive.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
The result-plus-error pattern is idiomatic Go: callers inspect the error before relying on a result whose validity depends on success.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
When an operation fails, the non-error results should have documented semantics; often they are their zero values.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
The blank identifier explicitly discards a result. It should not be used merely to hide an error that should have been handled.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Error wrapping can add operation-specific context while preserving the underlying cause for callers that need to inspect it.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Named results can clarify certain APIs but can also make control flow harder to follow when combined with naked returns.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Use a struct when named fields improve readability, especially when the result is likely to grow over time.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Tests should validate both successful values and error states, including malformed, boundary and partial-result cases.
A practical review should also consider how the choice affects package boundaries, observability, testing, future API changes and the experience of the developer who will maintain the code months later.
Engineering Perspective
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
Error wrapping can add operation-specific context while preserving the underlying cause for callers that need to inspect it.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
Named results can clarify certain APIs but can also make control flow harder to follow when combined with naked returns.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
Use a struct when named fields improve readability, especially when the result is likely to grow over time.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
Tests should validate both successful values and error states, including malformed, boundary and partial-result cases.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
A function can return one value, several values, or no values. The signature tells callers exactly what they receive.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
The result-plus-error pattern is idiomatic Go: callers inspect the error before relying on a result whose validity depends on success.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
When an operation fails, the non-error results should have documented semantics; often they are their zero values.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
The blank identifier explicitly discards a result. It should not be used merely to hide an error that should have been handled.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
Several tightly related values can be returned together, but a large set of loosely related outputs is often clearer as a struct with named fields.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
An error can be returned upward when the current layer lacks enough context to recover or report it meaningfully.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
Error wrapping can add operation-specific context while preserving the underlying cause for callers that need to inspect it.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
Named results can clarify certain APIs but can also make control flow harder to follow when combined with naked returns.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.
Use a struct when named fields improve readability, especially when the result is likely to grow over time.
A useful professional habit is to ask what a caller can safely assume, what the implementation is allowed to change, and what behavior must remain stable when the project evolves.