-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
Proposal Details
Abstract
This proposal aims to formalize Go's multi-return values (e.g., (T, error)) as lightweight "tuple structs" at the language level. The primary goal is to enable fluent, chainable handling of multi-value results (especially error-aware workflows) while maintaining backward compatibility. This addresses the current friction in error handling—where manual error checks (if err != nil) are repetitive and non-fluent—without replacing existing patterns.
Background
Go's idiomatic error handling relies on multi-return values like (result, error). While explicit, this pattern often leads to repetitive code:
a, err := step1()
if err != nil {
return err
}
b, err := step2(a)
if err != nil {
return err
}
// ... and so onDevelopers often work around this by manually implementing "result wrappers" (e.g., a Result[T] struct with Map/AndThen methods) to enable chaining. However, these wrappers are not standardized:
- They require boilerplate conversion between
(T, error)and the wrapper type. - They cannot seamlessly integrate with existing functions returning
(T, error). - They lack interoperability across libraries, limiting their utility.
Formalizing multi-return values as tuple structs would make this pattern native to the language, enabling fluent error handling while retaining compatibility with existing code.
Proposal
Treat multi-return value sequences (e.g., (T, error), (int, string)) as built-in "tuple structs" with the following core characteristics:
-
Tuple Struct Basics:
A tuple struct is a nameless, fixed-size collection of values, defined by the types of its elements. For example, a function returning(int, error)is treated as returningtuple(int, error)—a tuple struct with two elements. -
Seamless Compatibility:
- Existing code remains unchanged. Functions returning
(T, error)are implicitly returningtuple(T, error). - Assignment via
a, b := f()continues to work, now interpreted as "unpacking" the tuple struct into variables.
- Existing code remains unchanged. Functions returning
-
Core Feature: Fluent Chaining for Error Handling:
To enable fluent error-aware workflows, tuple structs with anerroras their last element (the most common pattern, e.g.,tuple(T, error)) would include built-in methods:AndThen[U any](f func(T) (U, error)) (U, error):
If the current tuple has no error, appliesfto the first element; otherwise, propagates the error.Map[U any](f func(T) U) (U, error):
If no error, appliesfto the first element (for non-error transformations); otherwise, propagates the error.- ...
Example usage:
func parse(s string) (int, error) { ... } func double(n int) (int, error) { ... } func addOne(n int) int { ... } // Fluent chain: parse → double → addOne val, err := parse("10").AndThen(double).Map(addOne) if err != nil { /* handle error */ }
-
No New Syntax:
Tuple structs reuse existing multi-return syntax. There is no need for new literals (e.g.,(1, err)remains a return statement, not a "tuple literal").
Rationale
- Fluent Error Handling: Reduces repetitive
if err != nilchecks by enabling chaining, while preserving Go's explicit error handling philosophy (errors are still unpacked at the end). - Standardization: Replaces fragmented manual
Resultwrappers with a language-native solution, ensuring interoperability across codebases. - Backward Compatibility: Existing code continues to work. Developers can adopt chaining incrementally.
- Simplicity: Builds on familiar multi-return patterns instead of introducing new concepts like
try/catchor heavyweight monads.
Compatibility
This proposal is fully backward compatible:
- Existing functions returning
(T, error)work with no changes. - Traditional
if err != nilchecks remain valid and are not deprecated. - Tuple structs are a隐形升级 (invisible upgrade) to multi-return values, not a breaking change.
Implementation Notes
- The compiler would recognize tuple struct types (e.g.,
tuple(int, error)) as distinct from other types. - Methods like
AndThenandMapwould be implicitly available for tuples where the last element iserror. - Unpacking logic (
a, b := tuple) remains unchanged but is now semantically an operation on a tuple struct.
Future Extensions (Non-Core)
The core proposal focuses on error-aware chaining. If adopted, future extensions could include:
- Allowing tuple structs to be stored in variables (e.g.,
var t tuple(int, error) = f()). - Supporting tuple structs as function parameters (e.g.,
func f(t tuple(int, error))). - Enabling unpacking in function calls (e.g.,
func g(a int, b error) { ... }; g(t...)). - Extending generic support for tuple structs (e.g.,
func Wrap[T any](f func() tuple(T, error))).
These extensions are not required for the core goal of improving error handling and can be evaluated separately.
Conclusion
Formalizing multi-return values as tuple structs with built-in chaining methods would make error handling more fluent and less repetitive, while preserving Go's simplicity and compatibility. By standardizing a pattern already used in manual workarounds, this proposal addresses a common pain point without disrupting existing code.