Home > Article > Backend Development > A brief analysis of closures in Golang
Before we actually talk about closures, let’s lay some groundwork:
[Related recommendations: Go video tutorial】
1.1 Prerequisite knowledge
Functional programming is a programming paradigm, a way of looking at problems. Every function is organized in small functions Become a larger function, the parameters of the function are also functions, and the function returns are also functions. Our common programming paradigms are:
Functional programming can be considered the opposite of object-oriented programming. Generally, only some programming languages will emphasize a specific programming method, and most languages are multi-paradigm languages. , can support a variety of different programming methods, such as JavaScript, Go, etc.
Functional programming is a way of thinking. It regards computer operations as the calculation of functions. It is a methodology for writing codes. In fact, I should talk about functional programming, and then talk about closures. Because closure itself is one of the characteristics of functional programming.
In functional programming, a function is a first-class object, which means that a function can be used as an input parameter value for other functions, or it can be retrieved from a function The return value is modified or assigned to a variable. (Wikipedia)
Generally pure functional programming languages do not allow direct use of program state and variable objects. Functional programming itself is to avoid the use of shared state,Variable state, avoid side effects as much as possible.
Functional programming generally has the following characteristics:
Functions are first-class citizens: functions are placed first and can be used as parameters, assigned values, and Passed, can be used as a return value.
No side effects: the function must remain purely independent, cannot modify the value of external variables, and does not modify external states.
Reference transparency: Function operation does not depend on external variables or states. For the same input parameters, the return value should be the same in any case.
Scope (scope), programming concept, generally speaking, The name used in a piece of program code is not always valid/available, and the code scope that limits the availability of the name is the scope of the name.
In layman’s terms, function scope refers to the range in which a function can work. A function is a bit like a box, one layer inside another. We can understand the scope as a closed box, that is, the local variables of the function, which can only be used inside the box and become an independent scope.
#The local variable within the function jumps out of the scope after leaving the function, and the variable cannot be found. (The inner function can use the local variables of the outer function, because the scope of the outer function includes the inner function). For example, the following innerTmep
cannot find the variable outside the function scope, but outerTemp
can still be used in inner functions.
Regardless of any language, there is basically a certain memory recycling mechanism, that is, recycling unused memory space. The recycling mechanism is generally the same as the scope of the function mentioned above. It is related. When a local variable goes out of its scope, it may be recycled. If it is still referenced, it will not be recycled.
The so-called scope inheritance means that the small box mentioned earlier can inherit the scope of the outer large box, and can be taken out directly from the small box Things in the big box, but the big box cannot take out the things in the small box, unless escape occurs (escape can be understood as giving a reference to the things in the small box, and the big box can be used as soon as it is obtained). Generally speaking, there are two types of scopes of variables:
Global scope: applies anywhere
Local scope: general It is a code block, within a function or package, The variables declared/defined inside the function are called local variables, the scope is limited to the inside of the function
1.2 Definition of closure
"In most cases we do not understand first and then define, but define first and then understand", first Define it,It doesn’t matter if you don’t understand it:
Closure is a reference toOne sentence expression:a function and its bundled surrounding environment (lexical environment, lexical environment) The combination. In other words, closures allow developers to access the scope of an outer function from an inner function. Closures are created when the function is created.
The words Go language cannot be found in the above definition. Smart students must know that closure has nothing to do with language. It is not unique to JavaScript or Go, but Functional programming language is unique, yes, you read it right, Any language that supports functional programming supports closures, Go and JavaScript are two of them, and the current version of Java also supports closures, But some people may think that it is not a perfect closure, the details are discussed in the article.
1.3 How to write closure
The following is A piece of closure code:
import "fmt" func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) } func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") var sum = func() int { fmt.Println("求结果...") result := 0 for _, v := range arr { result = result + v } return result } return sum }
Output result:
先获取函数,不求结果 等待一会 求结果... 结果: 15
It can be seen that the sum()
method inside can refer to the external function lazySum()
parameters and local variables, when lazySum()
returns function sum()
, the relevant parameters and variables are saved in the returned function and can be processed later transfer.
The above function may go one step further to reflect the bundled function and its surrounding status. We add a number of times count
:
import "fmt" func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) fmt.Println("结果:", sumFunc()) fmt.Println("结果:", sumFunc()) }func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { count++ fmt.Println("第", count, "次求结果...") result := 0 for _, v := range arr { result = result + v } return result } return sum }
What does the above code output? ? Will the number of times count
change? count
is obviously a local variable of the outer function, but in the memory function reference (bundling), the inner function is exposed. The execution results are as follows :
先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15 第 2 次求结果... 结果: 15 第 3 次求结果... 结果: 15
The result is count
In fact, it will change every time. To summarize this situation:
lazySum() was created once and executed 3 times, but if it is executed 3 times, it will be created differently. , what will it be like? Experiment:
import "fmt" func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) sumFunc1 := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc1()) sumFunc2 := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc2()) }func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { count++ fmt.Println("第", count, "次求结果...") result := 0 for _, v := range arr { result = result + v } return result } return sum }The execution results are as follows, each execution is the first time:
先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15 先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15 先获取函数,不求结果 等待一会 第 1 次求结果... 结果: 15It can be seen from the above execution results:
Close When the package is created, one copy of the referenced external variable count has already been created, that is, it does not matter if they are called individually.
Continue to raise a question, **If a function returns two functions, is this one closure or two closures? ** Let’s practice it below:
Return two functions at a time, one for calculating the result of the sum, and one for calculating the product:
import "fmt" func main() { sumFunc, productSFunc := lazyCalculate([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) fmt.Println("结果:", productSFunc()) }func lazyCalculate(arr []int) (func() int, func() int) { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { count++ fmt.Println("第", count, "次求加和...") result := 0 for _, v := range arr { result = result + v } return result } var product = func() int { count++ fmt.Println("第", count, "次求乘积...") result := 0 for _, v := range arr { result = result * v } return result } return sum, product }
The running results are as follows:
先获取函数,不求结果 等待一会 第 1 次求加和... 结果: 15 第 2 次求乘积... 结果: 0
As can be seen from the above results, closure is when a function returns a function. No matter how many return values (functions) there are, it is a closure. If the returned function uses external function variables, they will be bound together and affect each other. :
The closure binds the surrounding state. I understand that the function at this time has the state, so that the function has all the capabilities of the object, and the function has the state.
1.3.2 Pointers and values in closuresimport "fmt" func main() { i := 0 testFunc := test(&i) testFunc() fmt.Printf("outer i = %d\n", i) }func test(i *int) func() { *i = *i + 1 fmt.Printf("test inner i = %d\n", *i) return func() { *i = *i + 1 fmt.Printf("func inner i = %d\n", *i) } }
The running results are as follows:
test inner i = 1 func inner i = 2 outer i = 2
It can be seen that if it is a pointer, if the value of the address corresponding to the pointer is modified in the closure, it will also affect the value outside the closure. This is actually very easy to understand. There is no reference passing in Go, only value passing. When we pass a pointer, it is also passed by value. The value here is the value of the pointer (which can be understood as the address value).
When the parameter of our function is a pointer, the parameter will copy a copy of the pointer address and pass it as a parameter. Because the essence is still an address, internal modifications can still have an impact on the outside.
The data in the closure actually has the same address. The following experiment can prove it:
func main() { i := 0 testFunc := test(&i) testFunc() fmt.Printf("outer i address %v\n", &i) } func test(i *int) func() { *i = *i + 1 fmt.Printf("test inner i address %v\n", i) return func() { *i = *i + 1 fmt.Printf("func inner i address %v\n", i) } }
The output is as follows, so it can be inferred that if the closure refers to the pointer data of the external environment, it will only Copy a copy of the pointer address data instead of copying the real data (==Leave a question first: when is the timing of the copy==):
test inner i address 0xc0003fab98 func inner i address 0xc0003fab98 outer i address 0xc0003fab981.3.2 Closure delay
The following is a continuation of the previous experiment:
func main() { i := 0 testFunc := test(&i) i = i + 100 fmt.Printf("outer i before testFunc %d\n", i) testFunc() fmt.Printf("outer i after testFunc %d\n", i) }func test(i *int) func() { *i = *i + 1 fmt.Printf("test inner i = %d\n", *i) return func() { *i = *i + 1 fmt.Printf("func inner i = %d\n", *i) } }
After we create the closure, we change the data and then execute the closure. The answer must be that it really affects the execution of the closure, because they all They are pointers, all pointing to the same data:
test inner i = 1 outer i before testFunc 101 func inner i = 102 outer i after testFunc 102
Suppose we change the way of writing and let the variables in the external environment of the closure be modified after declaring the closure function:
import "fmt" func main() { sumFunc := lazySum([]int{1, 2, 3, 4, 5}) fmt.Println("等待一会") fmt.Println("结果:", sumFunc()) } func lazySum(arr []int) func() int { fmt.Println("先获取函数,不求结果") count := 0 var sum = func() int { fmt.Println("第", count, "次求结果...") result := 0 for _, v := range arr { result = result + v } return result } count = count + 100 return sum }
Actual execution As a result,
count will be the modified value: <pre class="brush:js;toolbar:false;">等待一会
第 100 次求结果...
结果: 15</pre>
This also proves that the closure does not actually declare
After this sentence, the 如果还没看明白没关系,我们再来一个例子: 上面的例子,我们闭包返回的是函数数组,本意我们想入每一个 以上两个例子,都是闭包延迟绑定的问题导致,这也可以说是 feature,到这里可能不少同学还是对闭包绑定外部变量的时机有疑惑,到底是返回闭包函数的时候绑定的呢?还是真正执行闭包函数的时候才绑定的呢? 下面的例子可以有效的解答: 输出结果如下: 第二次执行闭包函数的时候,明显 其实本质上,Go Routine的匿名函数的延迟绑定就是闭包的延迟绑定,上面的例子中, 总结一下上面的验证点: 2.1 好处 纯函数没有状态,而闭包则是让函数轻松拥有了状态。但是凡事都有两面性,一旦拥有状态,多次调用,可能会出现不一样的结果,就像是前面测试的 case 中一样。那么问题来了: Q:如果不支持闭包的话,我们想要函数拥有状态,需要怎么做呢? A: 需要使用全局变量,让所有函数共享同一份变量。 但是我们都知道全局变量有以下的一些特点(在不同的场景,优点会变成缺点): 闭包可以一定程度优化这个问题: 除了以上的好处,像在 JavaScript 中,没有原生支持私有方法,可以靠闭包来模拟私有方法,因为闭包都有自己的词法环境。 2.2 坏处 函数拥有状态,如果处理不当,会导致闭包中的变量被误改,但这是编码者应该考虑的问题,是预期中的场景。 闭包中如果随意创建,引用被持有,则无法销毁,同时闭包内的局部变量也无法销毁,过度使用闭包会占有更多的内存,导致性能下降。一般而言,能共享一份闭包(共享闭包局部变量数据),不需要多次创建闭包函数,是比较优雅的方式。 从上面的实验中,我们可以知道,闭包实际上就是外部环境的逃逸,跟随着闭包函数一起暴露出去。 我们用以下的程序进行分析: 执行结果如下: 先看看逃逸分析,用下面的命令行可以查看: 可以看到 变量 下面我们用命令行来看看汇编代码: 生成代码比较长,我截取一部分: 可以看到闭包函数实际上底层也是用结构体 uses the That is, when returning the function, in fact Returns a structure, which records the reference environment of the function. 4.1 Does Java support closures? There are many opinions on the Internet. In fact, although Java does not currently support return functions as return parameters, Java essentially implements the concept of closure, and the method used is The form of an inner class, because it is an inner class, is equivalent to having a reference environment, which is considered an incomplete closure. Currently there are certain restrictions, such as There are related answers on Stack Overflow: stackoverflow.com/questions/5… 4.2 What is the future of functional programming? The following is the content of the Wiki: Functional programming has long been popular in academia but has few industrial applications. The main reason for this situation is that functional programming is often considered to seriously consume CPU and memory resources [18]. This is because efficiency issues were not considered in the early implementation of functional programming languages, and they were function-oriented. Formula programming features, such as ensuring reference transparency, etc., require unique data structures and algorithms. [19] However, recently several functional programming languages have been used in commercial or industrial systems [20], for example: Other functional programming languages used in industry include multi-paradigm Scala[23], F#, and There are Wolfram Language, Common Lisp, Standard ML and Clojure, etc. From my personal point of view, I am not optimistic about pure functional programming, but I believe that almost every advanced programming need will have the idea of functional programming in the future. I especially look forward to Java embracing functional programming. Judging from the languages I know, the functional programming features in Go and JavaScript are deeply loved by developers (of course, if you write bugs, you will hate them). The reason why it has suddenly become popular recently is also because the world continues to develop and memory is getting larger and larger. The restrictions of this factor are almost liberated. I believe that the world is colorful. It is impossible for one thing to rule the world. It is more about a hundred schools of thought contending. The same is true for programming languages or programming paradigms. There may be masters in the future, and eventually history will Screen out those that are ultimately in line with the development of human society. For more programming related knowledge, please visit: Programming Video! ! count
of the external environment is bound to the closure. It is only bound when the function returns the closure function. This is Lazy binding
. func main() {
funcs := testFunc(100)
for _, v := range funcs {
v()
}
}
func testFunc(x int) []func() {
var funcs []func()
values := []int{1, 2, 3}
for _, val := range values {
funcs = append(funcs, func() {
fmt.Printf("testFunc val = %d\n", x+val)
})
}
return funcs
}
val
都不一样,但是实际上 val
都是一个值,==也就是执行到return funcs
的时候(或者真正执行闭包函数的时候)才绑定的 val
值==(关于这一点,后面还有个Demo可以证明),此时 val
的值是最后一个 3
,最终输出结果都是 103
:testFunc val = 103
testFunc val = 103
testFunc val = 103
import (
"fmt"
"time"
)
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
time.Sleep(time.Duration(3) * time.Second)
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
go func() {
time.Sleep(time.Duration(1) * time.Second)
count = count + 100
fmt.Println("go func 修改后的变量 count:", count)
}()
return sum
}
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
go func 修改后的变量 count: 101
第 102 次求结果...
结果: 15
count
被里面的 go func()
修改了,也就是调用的时候,才真正的获取最新的外部环境,但是在声明的时候,就会把环境预留保存下来。go func(){}
获取到的就是最新的值,而不是原始值0
。
2、闭包的好处与坏处?
3、闭包怎么实现的?
import "fmt"
func testFunc(i int) func() int {
i = i * 2
testFunc := func() int {
i++
return i
}
i = i * 2
return testFunc
}
func main() {
test := testFunc(1)
fmt.Println(test())
}
5
go build --gcflags=-m main.go
i
被移到堆中,也就是本来是局部变量,但是发生逃逸之后,从栈里面放到堆里面,同样的 test()
函数由于是闭包函数,也逃逸到堆上。go tool compile -N -l -S main.go
"".testFunc STEXT size=218 args=0x8 locals=0x38 funcid=0x0 align=0x0
0x0000 00000 (main.go:5) TEXT "".testFunc(SB), ABIInternal, $56-8
0x0000 00000 (main.go:5) CMPQ SP, 16(R14)
0x0004 00004 (main.go:5) PCDATA $0, $-2
0x0004 00004 (main.go:5) JLS 198
0x000a 00010 (main.go:5) PCDATA $0, $-1
0x000a 00010 (main.go:5) SUBQ $56, SP
0x000e 00014 (main.go:5) MOVQ BP, 48(SP)
0x0013 00019 (main.go:5) LEAQ 48(SP), BP
0x0018 00024 (main.go:5) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0018 00024 (main.go:5) FUNCDATA $1, gclocals·d571c0f6cf0af59df28f76498f639cf2(SB)
0x0018 00024 (main.go:5) FUNCDATA $5, "".testFunc.arginfo1(SB)
0x0018 00024 (main.go:5) MOVQ AX, "".i+64(SP)
0x001d 00029 (main.go:5) MOVQ $0, "".~r0+16(SP)
0x0026 00038 (main.go:5) LEAQ type.int(SB), AX
0x002d 00045 (main.go:5) PCDATA $1, $0
0x002d 00045 (main.go:5) CALL runtime.newobject(SB)
0x0032 00050 (main.go:5) MOVQ AX, "".&i+40(SP)
0x0037 00055 (main.go:5) MOVQ "".i+64(SP), CX
0x003c 00060 (main.go:5) MOVQ CX, (AX)
0x003f 00063 (main.go:6) MOVQ "".&i+40(SP), CX
0x0044 00068 (main.go:6) MOVQ "".&i+40(SP), DX
0x0049 00073 (main.go:6) MOVQ (DX), DX
0x004c 00076 (main.go:6) SHLQ $1, DX
0x004f 00079 (main.go:6) MOVQ DX, (CX)
0x0052 00082 (main.go:7) LEAQ type.noalg.struct { F uintptr; "".i *int }(SB), AX
0x0059 00089 (main.go:7) PCDATA $1, $1
0x0059 00089 (main.go:7) CALL runtime.newobject(SB)
0x005e 00094 (main.go:7) MOVQ AX, ""..autotmp_3+32(SP)
0x0063 00099 (main.go:7) LEAQ "".testFunc.func1(SB), CX
0x006a 00106 (main.go:7) MOVQ CX, (AX)
0x006d 00109 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX
0x0072 00114 (main.go:7) TESTB AL, (CX)
0x0074 00116 (main.go:7) MOVQ "".&i+40(SP), DX
0x0079 00121 (main.go:7) LEAQ 8(CX), DI
0x007d 00125 (main.go:7) PCDATA $0, $-2
0x007d 00125 (main.go:7) CMPL runtime.writeBarrier(SB), $0
0x0084 00132 (main.go:7) JEQ 136
0x0086 00134 (main.go:7) JMP 142
0x0088 00136 (main.go:7) MOVQ DX, 8(CX)
0x008c 00140 (main.go:7) JMP 149
0x008e 00142 (main.go:7) CALL runtime.gcWriteBarrierDX(SB)
0x0093 00147 (main.go:7) JMP 149
0x0095 00149 (main.go:7) PCDATA $0, $-1
0x0095 00149 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX
0x009a 00154 (main.go:7) MOVQ CX, "".testFunc+24(SP)
0x009f 00159 (main.go:11) MOVQ "".&i+40(SP), CX
0x00a4 00164 (main.go:11) MOVQ "".&i+40(SP), DX
0x00a9 00169 (main.go:11) MOVQ (DX), DX
0x00ac 00172 (main.go:11) SHLQ $1, DX
0x00af 00175 (main.go:11) MOVQ DX, (CX)
0x00b2 00178 (main.go:12) MOVQ "".testFunc+24(SP), AX
0x00b7 00183 (main.go:12) MOVQ AX, "".~r0+16(SP)
0x00bc 00188 (main.go:12) MOVQ 48(SP), BP
0x00c1 00193 (main.go:12) ADDQ $56, SP
0x00c5 00197 (main.go:12) RET
0x00c6 00198 (main.go:12) NOP
0x00c6 00198 (main.go:5) PCDATA $1, $-1
0x00c6 00198 (main.go:5) PCDATA $0, $-2
0x00c6 00198 (main.go:5) MOVQ AX, 8(SP)
0x00cb 00203 (main.go:5) CALL runtime.morestack_noctxt(SB)
0x00d0 00208 (main.go:5) MOVQ 8(SP), AX
0x00d5 00213 (main.go:5) PCDATA $0, $-1
0x00d5 00213 (main.go:5) JMP 0
new
创建出来的: i
on the heap: 4. Let’s talk briefly
final
declaration, or a clearly defined value, which can be passed:
The above is the detailed content of A brief analysis of closures in Golang. For more information, please follow other related articles on the PHP Chinese website!