Description
I'm proposing adding two built-in functions ?
and !
for better error handling.
?
and !
are built-in function names, by default they refer to two functions in errors
package.
?
= errors.Log
!
= errors.Return
package errors
func Log(err error) {
log.Printf("%v\n", err)
}
func Return(err error) error {
return err
}
You can see the difference from their definitions, that errors.Log
simply logs the error and errors.Return
returns a new error.
We use ?
or !
where err
should place, normally as the last return value receiver from a function call, and they receive the err
value.
func GetUser(name string) (*User, error) {
user, err := getUser(name)
if err != nil {
return nil, err
}
age, err := getAge(name)
// Just log the error
if err != nil {
log.Printf("%v\n", err)
}
user.Age = age
return user, nil
}
can become:
func GetUser(name string) (*User, error) {
// If the err value that ! receives is not nil, produce a new error and this function returns here.
user, ! := getUser(name)
// if the err value that ? receives is not nil, logs the error and this function goes on.
age, ? := getAge(name)
user.Age = age
return user, nil
}
What !
does here is to receive the last return value(the err
) from getUser(name)
, and if err != nil
, execute errors.Return
with err
as its sole argument and produce a new err
, then return the outside function GetUser
with its last return value(error
type) being the new err
and other return values as what they are. If the value !
receives is nil, nothing will happen.
The difference between ?
and !
is that if the err
value ?
receives is not nil, it just executes errors.Log
for logging but GetUser
goes on, it won't return here.
We use ?
and !
to mark that !
will return the outside function if the err value it receives is not nil and ?
won't.
Customization
Since ?
and !
are built-in function names, they can be assigned to other error handling functions at package or function level.
In many cases we want to return a new error that wraps error contexts. We can do that with !
.
package errors
func WrapReturn(format string, args ...interface{}) func(error) error {
return func(err error) error {
return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}
}
We now use errors.WrapReturn
to return an error handling function and assign it to !
at any time:
func GetUser(name string) (*User, error) {
// This is an assignment statement, we need a little work to make `!` a valid function name.
// To distinguish ! assignment and ! call below, we may need to change ! call to `!()`(for discussion).
! = errors.WrapReturn("GetUser (name=%s)", name)
user, ! := getUser(name)
age, ? := getAge(name)
user.Age = age
return user, nil
}
As you can see, we can assign !
or ?
to our customized error handling functions at package level or function level, at any time. So you can name a package level error handling strategy and override it in a specific function.
Summary
-
This is not a formal Go proposal, I'm not capable of doing that. This is just a new idea for discussion.(Sorry for the proposal title.)
-
This approach won't meet all error handling scenarios. But it do has some advantages:
- Compatible with
if err != nil
common practice, you can combine both when?
or!
is not sufficient. - Can mitigate many
if err != nil { return err }
cases. This is its first goal. - Very customizable. You can name your project or package or function level error handling strategy, and inherit or override the larger scope strategy at any time.
- Keeps the number of value receivers from a function call same as the number of the called function return values.
- Can distinguish clearly whether an error will cause early return, just like using
if err != nil { return err }
.
- Compatible with