val, err := function()
if err != nil {
//handler codes...
}
Error handling consists of 3 key parts, trigger, handler and binding rule. In Go 1, trigger is if err != nil
, handler is a piece of code in {}
, and binding rule is obvious. Handler holds the control flow switch.
Actually, I donot think error handling in Go 1 has any functional problem. The only problem is repetition, especially repetition of triggers. Go 1 allows us to place trigger and assignment in one line, but that does not really solve the problem.
if val, err := function(); err != nil {
//handler codes...
}
Triggers are the same, so it's a good idea to represent the trigger as one word. With more parameters, trigger can becomme more powerful. Trigger with 1 parameter can do nothing more than detect error. Trigger with 2 parameters can specify handler. Trigger with more than 2 parameters can specify a handler chain, or deliver extra parameters to the handler.
check(function, handler1, handler2, handler3)
func handler(path string, err error) error { return fmt.Errorf("%s: %v", path, err); }
check(function, handler, path)
Handlers are not always the same. What should we do is to make common handlers sharable, leaving special ones unchanged. Traditionaly, we create function for sharing code. The most common error handler, which just pass error to upstream, can be compiler-predefined. Implements of an interface may share one error handler. Main drawback of using function as error handler is function cannot change the control flow of its caller directly.
A simple clear binding rule may be important than trigger and handler.
for (i := 0; i < 2; i++) {
handle err {...} //A
check function() //Whould B be invoted?
handle err {...} //B
}
Complicated binding rule looks powerful, but help little in fact. When we have to think hard to figure out the consequence of error hanling, error hanling become poison.
Error handling should act as an easy exit in most cases. A complicated mechanism looks like defer/recover but works differently may be confusing. I suggest a simple way for error handling without new keywords.
func getDivisorFromDB(key string) (uint, error) {
//...
}
func GetDivisor(key string) (uint, error) {
exit := func(err error) (uint, error) {
return 1, fmt.Errorf("fail to get divisor with key \"%s\": %v", key, err)
}
divisor := check(getDivisorFromDB(key), exit)
//...
return divisor, nil
}
In code above, "check" is a builtin function like "make", accepting 1 or 2 parameters. The first is the function to be checked, the second is the error handler. An error handler is a function having the same output type with the function to be checked. If the error handler is not specified, a default one will be created to pass through error.
divisor := check(getDivisorFromDB(key), exit) //divisor := check(getDivisorFromDB(key))
is equal to
divisor, err := getDivisorFromDB(key)
if err != nil {
return exit(err) //return err
}
Chained error handling is not the common case, because the defer mechanism will handle resouce release. We just want a quick exit when error occurs. If someone really needs chained handling, he can do it explictly in the exit function.
This does not cover all cases of error handling, but improves exiting on error which contributes the most repeated code. Uncommon dedicate operations should be done with old style. If many breaks or continues are need in a loop, that fat loop body should be refactored into one or more functions where the check&exit works.
Usually, we don't need any custom exit function.