In Go, there is a special concept of named return value in function where a returned value can have name. For example, below is a named function in Go.
func returnNamed() (i int) {
i = 1
return
}
When the function returns, the return value i will have a value of 1.
Also, Go has a concept of defer which will execute a function just before the calling function exits. This is similar to what finally block does in other languages such as Java. For example, a defer function can be
func deferFunc() {
defer func() { fmt.Println("In defer") }()
fmt.Println("Start")
}
Both return and defer can change the function execution flow. What if a function has both return and defer and the return value is updated in a defer function?
See below code snippet
func returnNamed() (i int) {
i = 1
defer func() { i++ }()
return i
}
What would the return value be when returnNamed() is called?
And what if a defer function is called in a normal function with return statement. Like below:
func returnNormal() int {
i := 1
defer func() { i++ }()
return i
}
What will be the return value of above function call?
Let's write a complete code example and get the output first.
package main
import "fmt"
func returnNormal() int {
i := 1
defer func() { i++ }()
return i
}
func returnNamed() (i int) {
i = 1
defer func() { i++ }()
return i
}
func main() {
fmt.Printf("returnNormal() = %d\n", returnNormal())
fmt.Printf("returnNamed() = %d\n", returnNamed())
}
The output will be:
returnNormal() = 1
returnNamed() = 2
Now let's check out why above output is displayed. To understand why, we may need to check their assembly code when complied.
Below is the assembly code for the returnNormal() function.
0x0000 00000 (main.go:5) TEXT "".returnNormal(SB), $40-8
0x0000 00000 (main.go:5) MOVQ TLS, CX
0x0009 00009 (main.go:5) MOVQ (CX)(TLS*2), CX
0x0010 00016 (main.go:5) CMPQ SP, 16(CX)
0x0014 00020 (main.go:5) JLS 134
0x0016 00022 (main.go:5) SUBQ $40, SP
0x001a 00026 (main.go:5) MOVQ BP, 32(SP)
0x001f 00031 (main.go:5) LEAQ 32(SP), BP
0x0024 00036 (main.go:5) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
0x0024 00036 (main.go:5) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0024 00036 (main.go:5) MOVQ $0, "".~r0+48(SP)
0x002d 00045 (main.go:6) MOVQ $1, "".i+24(SP)
0x0036 00054 (main.go:7) LEAQ "".i+24(SP), AX
0x003b 00059 (main.go:7) MOVQ AX, 16(SP)
0x0040 00064 (main.go:7) MOVL $8, (SP)
0x0047 00071 (main.go:7) LEAQ "".returnNormal.func1·f(SB), AX
0x004e 00078 (main.go:7) MOVQ AX, 8(SP)
0x0053 00083 (main.go:7) PCDATA $0, $0
0x0053 00083 (main.go:7) CALL runtime.deferproc(SB)
0x0058 00088 (main.go:7) TESTL AX, AX
0x005a 00090 (main.go:7) JNE 118
0x005c 00092 (main.go:8) MOVQ "".i+24(SP), AX
0x0061 00097 (main.go:8) MOVQ AX, "".~r0+48(SP)
0x0066 00102 (main.go:8) PCDATA $0, $0
0x0066 00102 (main.go:8) XCHGL AX, AX
0x0067 00103 (main.go:8) CALL runtime.deferreturn(SB)
0x006c 00108 (main.go:8) MOVQ 32(SP), BP
0x0071 00113 (main.go:8) ADDQ $40, SP
0x0075 00117 (main.go:8) RET
0x0076 00118 (main.go:7) PCDATA $0, $0
0x0076 00118 (main.go:7) XCHGL AX, AX
0x0077 00119 (main.go:7) CALL runtime.deferreturn(SB)
0x007c 00124 (main.go:7) MOVQ 32(SP), BP
0x0081 00129 (main.go:7) ADDQ $40, SP
0x0085 00133 (main.go:7) RET
0x0086 00134 (main.go:7) NOP
0x0086 00134 (main.go:5) PCDATA $0, $-1
0x0086 00134 (main.go:5) CALL runtime.morestack_noctxt(SB)
0x008b 00139 (main.go:5) JMP 0
From above code, we can see the return value location is at ~r0+48(SP), and if we check what happens when return is called.
0x005c 00092 (main.go:8) MOVQ "".i+24(SP), AX
0x0061 00097 (main.go:8) MOVQ AX, "".~r0+48(SP)
It moves the value at i+24(SP) to AX, which is moving 1 to AX, why 1? Because when the function is called, it first moves $1 to i+24(SP) where $1 is 1. Then it moves AX to ~r0+48(SP). This means the value 1 is moved to the return value location. When the function call returns, the value at this location will be returned.
What about returnNamed(), the assembly code is:
0x0000 00000 (main.go:11) TEXT "".returnNamed(SB), $32-8
0x0000 00000 (main.go:11) MOVQ TLS, CX
0x0009 00009 (main.go:11) MOVQ (CX)(TLS*2), CX
0x0010 00016 (main.go:11) CMPQ SP, 16(CX)
0x0014 00020 (main.go:11) JLS 115
0x0016 00022 (main.go:11) SUBQ $32, SP
0x001a 00026 (main.go:11) MOVQ BP, 24(SP)
0x001f 00031 (main.go:11) LEAQ 24(SP), BP
0x0024 00036 (main.go:11) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
0x0024 00036 (main.go:11) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0024 00036 (main.go:12) MOVQ $1, "".i+40(SP)
0x002d 00045 (main.go:13) LEAQ "".i+40(SP), AX
0x0032 00050 (main.go:13) MOVQ AX, 16(SP)
0x0037 00055 (main.go:13) MOVL $8, (SP)
0x003e 00062 (main.go:13) LEAQ "".returnNamed.func1·f(SB), AX
0x0045 00069 (main.go:13) MOVQ AX, 8(SP)
0x004a 00074 (main.go:13) PCDATA $0, $0
0x004a 00074 (main.go:13) CALL runtime.deferproc(SB)
0x004f 00079 (main.go:13) TESTL AX, AX
0x0051 00081 (main.go:13) JNE 99
0x0053 00083 (main.go:14) PCDATA $0, $0
0x0053 00083 (main.go:14) XCHGL AX, AX
0x0054 00084 (main.go:14) CALL runtime.deferreturn(SB)
0x0059 00089 (main.go:14) MOVQ 24(SP), BP
0x005e 00094 (main.go:14) ADDQ $32, SP
0x0062 00098 (main.go:14) RET
0x0063 00099 (main.go:13) PCDATA $0, $0
0x0063 00099 (main.go:13) XCHGL AX, AX
0x0064 00100 (main.go:13) CALL runtime.deferreturn(SB)
0x0069 00105 (main.go:13) MOVQ 24(SP), BP
0x006e 00110 (main.go:13) ADDQ $32, SP
0x0072 00114 (main.go:13) RET
0x0073 00115 (main.go:13) NOP
0x0073 00115 (main.go:11) PCDATA $0, $-1
0x0073 00115 (main.go:11) CALL runtime.morestack_noctxt(SB)
0x0078 00120 (main.go:11) JMP 0
The major difference here is that when return is called, there is no operation of moving the existing value into the return location. Instead, it calls the deferred function and the value of i gets updated. When the function returns, the value at location where i is is returned. In this case, the value is 2.
Hope this helps you understand why the normal and named functions are behaving differently.