by Leon Rosenshein

Naked Returns

TIL that you don’t actually need to list the things you return from a method when you use the return keyword. That’s called a naked return. At first I thought of Scala’s return and how you generally shouldn’t use it, but it’s clearly not that. It’s not (basically) returning the last thing calculated when you exit a function. That has lots of potential issues by itself and has led to some odd looking code, but that’s a different story.

Go’s naked return is conceptually very different. You can use a naked return if you not only declare the type of the returned value(s), but the variable names as well. What happens is that under the covers the compiler will create those variables, give them their default values, and then, when you just return` with no parameters, it will use the values of those variables.

1  package main
2
3  import (
4      "fmt"
5  )
6
7  func calculate(l, r int) (sum, prod int, block string) {
8
9      if l == 0 {
10         return
11     }
12
13     if l == 1 {
14         return sum, prod, block
15     }
16
17     sum = l + r
18     prod = l * r
19     block = "outer"
20
21     if l == 2 {
22         block := "inner2"
23         return sum, prod, block
24     }
25
26     if l == 3 {
27         block = "inner3"
28     }
29
30     if l == 4 {
31         block = "inner4"
32         return
33     }
34
35     if l == 5 {
36         // block := "inner5"
37         return
38     }
39
40     return
41 }
42
43 func main() {
44     sum, prod, block := calculate(0, 3)
45     fmt.Printf("For l = 0 - Sum: %d, Product %d. From %s\n", sum, prod, block)
46     sum, prod, block = calculate(1, 3)
47     fmt.Printf("For l = 1 - Sum: %d, Product %d. From %s\n", sum, prod, block)
48     sum, prod, block = calculate(2, 3)
49     fmt.Printf("For l = 2 - Sum: %d, Product %d. From %s\n", sum, prod, block)
50     sum, prod, block = calculate(3, 3)
51     fmt.Printf("For l = 3 - Sum: %d, Product %d. From %s\n", sum, prod, block)
52     sum, prod, block = calculate(4, 3)
53     fmt.Printf("For l = 4 - Sum: %d, Product %d. From %s\n", sum, prod, block)
54     sum, prod, block = calculate(5, 3)
55     fmt.Printf("For l = 5 - Sum: %d, Product %d. From %s\n", sum, prod, block)
56     sum, prod, block = calculate(6, 3)
57     fmt.Printf("For l = 6 - Sum: %d, Product %d. From %s\n", sum, prod, block)
58 }

For l = 0 - Sum: 0, Product 0. From
For l = 1 - Sum: 0, Product 0. From
For l = 2 - Sum: 5, Product 6. From inner2
For l = 3 - Sum: 6, Product 9. From inner3
For l = 4 - Sum: 7, Product 12. From inner4
For l = 5 - Sum: 8, Product 15. From outer
For l = 6 - Sum: 9, Product 18. From outer

In this contrived example, an immediate return, either naked (line 10) or explicit (line 14) gives you the default return values. Then depending on the value of l, you get different combinations of where the block variable is set and what and where the return is, but other than for l==0 and l==1, sum and prod are always calculated up front and are returned whether explicitly set on the return or not. And just in case you shadow the implicit declaration with a scope, if line 36 is uncommented you get a compile time error

./prog.go:37:3: block is shadowed during return

Which ensures you don’t do it by accident.

Seems handy, right. So why not always use them? Because they’re hard to read. The farther the return is from the declaration, the harder it is and the more cognitive load there is when you come across a naked return. And if I’ve said it once I’ve said it a bunch of times, always try to reduce cognitive load. There are plenty of other places where you’ll need those cycles, so when you can make things obvious, you should.

If you want to see it in action, check out this playground