Description
The must operator is to error handling what the go keyword is to concurrency, and the defer keyword is to resource cleanup. It turns any function into a Must*-style function with smart error handling.
NOTE: this isn't a change to the way error handling works, only to the way error handling looks.
The Must operator (#) is a unary prefix operator that can be applied to a function call. The last return value of the called function must be of type error
. The must operator "peels off" the returned error from the result list, and handles that error - if it is not nil - according to the error handling strategy of the surrounding function.
There are 2 possible error handling strategies:
- Return zero values, plus the error
- Panic with the error
The error handling strategy is determined by the surrounding function's return types: if the last return value is of type error
, it's error handling strategy is case 1, otherwise it's case 2.
Example:
result := #foo(1,2,3)
// would be equivalent to
result, err := foo(1,2,3)
if err != nil {
return 0, "", person{}, err // if the surrounding function returns an error
}
// -- or --
result, err := foo(1,2,3)
if err != nil {
panic(err) //if the surrounding function does not return an error
}
Some Pros:
- Error handling works the same way as before, but can be written a bit more tersely
- A function could very easily change its error handling strategy without changing its contents
- Adding or removing return values would no longer cascade into code that returns early because of errors. This code very often (always?) returns zero values, plus the error
- Example code that seeks to be terse would stop using the "drop the error" idiom:
result, _ := canFail() // silent failure!
- Libraries would no longer have to write "Must*" variants of functions: any function could be called with the Must Operator
- This is a backwards compatible change
- This may discourage the use of panic, as an error propagation mechanism
- Certain kinds of error-heavy code could have a better signal-to-noise ratio, increasing readability:
average := #getTotal() / #getCount()
- The programmer still has to acknowledge that an error can happen, but can use the Must operator to handle it.
Some Cons:
- Tools would need to be updated
- This may seem cryptic to new Gophers
- This may encouraging more use of panic by hiding the difference between panic and returning an error
- This may discourage the cases where errors require additional data as they propagate up the call stack:
return fmt.Errorf("while deleting user %s: %v", user, err)
// -- or --
return &CompoundError{"reason", err}
Conclusion
I believe there's precedent for the Must operator in golang. The defer
and go
keywords transform the way function calls work. The go
keyword hides a load of complexity from the user, but always does the right thing. The defer
keyword makes code less error-prone by ensuring the cleanup code will be called on every path. The Must operator is similar: it does the right thing based on the surrounding function, and it ensures that the error is handled.