Behavior of defer function in named return function

  sonic0002        2018-11-20 09:04:07       4,079        0    

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.

GOLANG  DEFER  NAMED RETURN  DIFFERENCE 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

I think I find the bug