This panic recover code works with named return values.
func main() {
result, err := foo()
fmt.Println("result:", result)
if err != nil {
fmt.Println("err:", err)
}
}
func foo() (result int, err error) {
defer func() {
if e := recover(); e != nil {
result = -1
err = errors.New(e.(string))
}
}()
bar()
result = 100
err = nil
return
}
func bar() {
panic("panic happened")
}
Output
result: -1
err: panic happened
But why this code with local variables does not work?
func main() {
result, err := foo()
fmt.Println("result:", result)
if err != nil {
fmt.Println("err:", err)
}
}
func foo() (int, error) {
var result int
var err error
defer func() {
if e := recover(); e != nil {
result = -1
err = errors.New(e.(string))
}
}()
bar()
result = 100
err = nil
return result, err
}
func bar() {
panic("panic happened")
}
Output
result: 0
Any explanation to help me understanding the reason / basic concept of it? In the go tour basics the explanation is as followed.
Named return values Go's return values may be named. If so, they are treated as variables defined at the top of the function.
So it should be the same, right?
Spec: Return statements details this:
There are three ways to return values from a function with a result type:
- The return value or values may be explicitly listed in the "return" statement. Each expression must be single-valued and assignable to the corresponding element of the function's result type.
- The expression list in the "return" statement may be a single call to a multi-valued function. The effect is as if each value returned from that function were assigned to a temporary variable with the type of the respective value, followed by a "return" statement listing these variables, at which point the rules of the previous case apply.
- The expression list may be empty if the function's result type specifies names for its result parameters. The result parameters act as ordinary local variables and the function may assign values to them as necessary. The "return" statement returns the values of these variables.
So basically if you use a return
statement that explicitly lists the return values, those will be used, regardless if the result parameters are named or not.
If the result parameters are named, they act as ordinary local variables: you can read and write them. If the result parameters are named, you may use a "naked" return
statement, without listing the values to return. If you do so, then the actual return values will be the values of the (named) result parameters. The same thing applies if your function does not reach a return
statement due to panicing and recovering: once the deferred functions run, the actual return values will be the values of the named result parameters (which the deferred functions can change and "have a say" in what to return).
If you don't use named result parameters but you declare local variables, they are not special in this way: when the function returns, those are not used "automatically" as the result values (like they would be if they would be named result parameters and not local variables). So if you change them in a deferred function, that will not have any effect on the actual values returned. In fact, if you don't use named result parameters and your function panics and recovers, you can't specify the return values, they will be the zero values of the result types. That's why you see result: 0
(0
is the zero value for int
) and no error (because error
is an interface type and zero value for interface types is nil
and you don't print the error if it's nil
).
See related: How to return a value in a Go function that panics?