SoFunction
Updated on 2025-03-01

The underlying principle of closures in Go

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!