5
package main

import (
    "fmt"
    "log"
)

func catch(err *error) {
    if r := recover(); r != nil {
        *err = fmt.Errorf("recovered panic: %v", r)
    }
}

func panicIf42(n int) {
    if n == 42 {
        panic("42!")
    }
}

func NormalReturns(n int) error {
    var err error
    defer catch(&err)
    panicIf42(n)
    return err
}

func NamedReturns(n int) (err error) {
    defer catch(&err)
    panicIf42(n)
    return
}

func main() {
    err := NamedReturns(42)
    log.Printf("NamedReturns error: %v", err)
    err = NormalReturns(42)
    log.Printf("NormalReturns error: %v", err)
}

output:

2009/11/10 23:00:00 NamedReturns error: recovered panic: 42!
2009/11/10 23:00:00 NormalReturns error: <nil>

Playground link

NormalReturns returns a nil error, but I would expect both NamedReturns and NormalReturns to return a non-nil error.

I thought named returns was just a code readability feature that declares and initializes returns for you, but it seems there's more to it. What am I missing?

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
vedran
  • 751
  • 1
  • 5
  • 16
  • I'll let someone who can speak from specification rather than intuition add a proper answer, but my intuition is that the `defer` is executed after `return err` already happened; so changing `err` no longer has any effect -- the copy to be returned was already copied to the stack, so modifying the local variable you declared with `var err error` no longer does anything useful. Whereas in the named-return case, `&err` refers to the copy on the stack. – Charles Duffy Dec 23 '19 at 21:54
  • @CharlesDuffy exactly. the behavior is `Deferred function calls are executed in Last In First Out order after the surrounding function returns.` – Not_a_Golfer Dec 23 '19 at 22:06
  • Nothing _hides_ a panic. It's just that to store the recovered value, you need a named return variable. – Jonathan Hall Dec 24 '19 at 09:01

1 Answers1

5

I thought named returns was just a code readability feature that declares and initializes returns for you, but it seems there's more to it. What am I missing?

If you name the result parameters, their actual value at the time of returning to the caller will determine the returned values. Meaning you can change their values like other local variables, and if the expression list of the return statement is empty, their last assigned values will be used. Also if there are deferred functions, they can modify the values of the named result parameters after the return statement and before the function returns to its caller, and those modifications will be preserved. It also allows to modify return values in case of a panic, see How to return a value in a Go function that panics?

Spec: Return statements:

Regardless of how they [the return values] are declared, all the result values are initialized to the zero values for their type upon entry to the function. A "return" statement that specifies results sets the result parameters before any deferred functions are executed.

And Spec: Defer statements:

For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned.

In NormalReturns(): The return value is initialized to its zero value (which is nil for all interface types, including the builtin error type), and since the return statement is not reached (due to a panic in panicIf42()), it will stay nil. It doesn't matter if the local variable err is changed, that is not the result variable. It's just an ordinary variable. It will have no effect on the value returned

In general, if a function does not have named result variables, and if this function does not reach a return statement (e.g. due to a panic), it cannot have return values other than (meaning different from) the zero values of the result types.

In NamedReturns() the deferred catch() will modify the named result variable err. The changes are "preserved": whatever the named result variables hold will be returned when the function ends (which happens after calling deferred functions, if there are any). So even though the return statement is not reached here either, the catch() function changes the err result variable, and whatever is assigned to it will be used as the value returned.

More on the topic:

Go blog: Defer, Panic and Recover:

Deferred functions may read and assign to the returning function's named return values.

And also in Effective Go: Recover:

If doParse panics, the recovery block will set the return value to nil—deferred functions can modify named return values.

icza
  • 389,944
  • 63
  • 907
  • 827
  • that did not help in understanding the actual behavior. Charles Duffy explanations kind of makes sense, however it seems like there is subtle implementation difference that remains hard to figure out. –  Dec 23 '19 at 22:24
  • @mh-cbon Added more details. – icza Dec 23 '19 at 22:35
  • definitely helped. To rephrase, `NormalReturns` does not return the locally defined `err` variable because it does not reach the return statement which would copy it to the result values, it instead returns the zero values because the panic broke the flow. However, in `NamedReturns` when the return happens, because this is a named variable, it returns that variable value, which can be modified by the defer etc. thank you –  Dec 23 '19 at 22:56
  • 1
    @mh-cbon Yes, I also highlighted that part in my answer. – icza Dec 23 '19 at 22:57
  • @icza i may be misreading something here, but the bold part and the the paragraph after it seem to suggest that if the return statement was reached the deferred function has the ability to modify the returned value even though it's not a named result parameter? Is that actually the case? – mkopriva Dec 23 '19 at 22:58
  • @mkopriva That paragraph tells that if the return parameters are not named and a `return` is not reached, the values actually returned will always be the zero values (the deferred functions can't modify something unnamed). – icza Dec 23 '19 at 23:01
  • @icza ah yes, my bad, I did indeed misunderstood the point there. Thanks. – mkopriva Dec 23 '19 at 23:05
  • playing with https://play.golang.org/p/Iz-omlTTPu0 following mkopriva comment, and `the deferred function may access and modify the result parameters before they are returned` poorly makes sense anymore.. I suggest to slightly modify that quotes to use **named result parameters**. or to quote more of it `For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal...` –  Dec 23 '19 at 23:16
  • @mh-cbon Extended the quote. – icza Dec 23 '19 at 23:27