1. What is a closure?
A function refers to external local variables, and this phenomenon is called a closure.
For example, in the following code,adder
The function returns an anonymous function, and the anonymous function refers to theadder
Local variables in functionssum
, then this function is a closure.
package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } }
The external local variables referenced in this closure will not followadder
The return of the function is destroyed from the stack.
We try to call this function and find out every time we call it,sum
The values of the closure function will be kept for use.
func main() { valueFunc:= adder() (valueFunc(2)) // output: 2 (valueFunc(2)) // output: 4 }
2. Complex closure scenarios
Writing a closure is relatively easy, but it is far from enough to just write a simple closure function. If you don’t understand the true principle of closure, it is easy to misjudgment the execution logic of the function in some complex closure scenarios.
Let’s not talk about anything else, just take this example as an example?
What do you think it will print?
Is it 6 or 11?
import "fmt" func func1() (i int) { i = 10 defer func() { i += 1 }() return 5 } func main() { closure := func1() (closure) }
3. The underlying principle of closure?
Or use the above example to analyze
package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { valueFunc:= adder() (valueFunc(2)) // output: 2 }
We first perform escape analysis on it, it is easy to find outsum
Asadder
Function local variables are not allocated on the stack, but on the heap.
This solves the first doubt:Whyadder
After the function returns,sum
Will it not be destroyed?
$ go build -gcflags="-m -m -l" # command-line-arguments ./:8:3: adder.func1 capturing by ref: sum (addr=true assign=true width=8) ./:7:9: func literal escapes to heap: ./:7:9: flow: ~r0 = &{storage for func literal}: ./:7:9: from func literal (spill) at ./:7:9 ./:7:9: from return func literal (return) at ./:7:2 ./:6:2: sum escapes to heap: ./:6:2: flow: {storage for func literal} = &sum: ./:6:2: from func literal (captured by a closure) at ./:7:9 ./:6:2: from sum (reference) at ./:8:3 ./:6:2: moved to heap: sum ./:7:9: func literal escapes to heap ./:15:23: valueFunc(2) escapes to heap: ./:15:23: flow: {storage for ... argument} = &{storage for valueFunc(2)}: ./:15:23: from valueFunc(2) (spill) at ./:15:23 ./:15:23: flow: {heap} = {storage for ... argument}: ./:15:23: from ... argument (spill) at ./:15:13 ./:15:23: from (valueFunc(2)) (call parameter) at ./:15:13 ./:15:13: ... argument does not escape ./:15:23: valueFunc(2) escapes to heap
But another problem emerges again. Even if it is not destroyed, then if the closure function is stored,sum
The copied value, then every time the closure function is called, thesum
It should be the same, and two calls should return 2 instead of accumulating records.
Therefore, it is possible to guess that the closure function structure is stored in thesum
pointer.
In order to verify this conjecture, we can only add it to assembly.
By executing the following command, you can output the corresponding assembly code
go build -gcflags="-S"
There is quite a lot of output content. I extracted the most critical line of code below, which defines the structure of the closure function.
Where F is a pointer to the function, but this is not the point. The point is that sum is indeed a pointer, which verifies our guess.
{ F uintptr; "".sum *int }(SB), CX
4. The mystery is revealed
With the background knowledge in the third section above, I believe you have the answer to the question given in the second section.
First, since i is declared on the return value defined by the function, according to the caller-save
The i variable will be stored in themain
The stack space of the function.
Then,func1
ofreturn
Assign 5 again to i, at this time i = 5
Because the closure function stores a pointer to this variable i.
Therefore, in the end, i is automatically incremented in defer, which is directly updated to the pointer of i. At this time, i = 5+1, so the final printout result is 6
import "fmt" func func1() (i int) { i = 10 defer func() { i += 1 }() return 5 } func main() { closure := func1() (closure) }
5. Change the question again
If you understand the above question, let’s take a look at the following question.
func1
We will not write the variable name i for the return value, and then return the specific literal value first, but now it is changed to the variable i. These two small changes will cause the operation results to be very different. You can think about the results.
import "fmt" func func1() (int) { i := 10 defer func() { i += 1 }() return i } func main() { closure := func1() (closure) }
If you write the variable name in the return value, the variable will be storedmain
If you don't write it, then i can only be stored in func1
in the stack space, at the same time,return
The value of , will not be applied to the original variable i, but will be stored in the function in another stack memory.
So you're indefer
The original i will increase automatically and will not workfunc1
on the return value.
So the print result can only be 10.
Have you answered correctly?
6. The last question
I don't know if you have found that the sum in the first section is stored in heap memory, while the following examples are stored in stack memory.
Why is this?
After careful comparison, it is not difficult to find that Example 1 returns a closure function. The closure function must be used elsewhere after the add is returned. In this case, in order to ensure the normal operation of the closure function, i cannot be recycled no matter where the closure function is, so the Go compiler will intelligently allocate it on the heap.
The other examples below only involve the characteristics of closures and do not directly return the closure function, so it can be allocated on the stack, which is very reasonable.
This is the end of this article about the underlying principles of closures in Go. For more related content on the underlying principles of Go closures, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!