Deferred insight: 10 years to understand named return behaviour
Despite writing Go for about 10 years, the number of times I've personally written code with named returns is probably countable on one hand. In all cases I can remember it to handle additional errors that might occur in a defer block, like in this code:
func fallible() (err error) {
defer func() {
cleanupErr = fallibleCleanup()
if cleanupErr != nil {
err = errors.Join(err, cleanupErr)
}
}()
err = otherFallible()
return err
}
My understanding was limited to the fact that the err variable would be initialised at the start of the function, and therefore would be available in the defer block to be set before returning. However, while reading some code today, I realised for the first time that return in the main block actually sets the value of the named variables, and the code above is equivalent to:
func fallible() (err error) {
defer func() {
cleanupErr := fallibleCleanup()
if cleanupErr != nil {
err = errors.Join(err, cleanupErr)
}
}()
return otherFallible()
}
Notice the absence of the final assignment to err. This makes sense, because if the value of err and the return from otherFallible were different, it would result in total nonsense. I'd just never really thought about it.