聊聊Go里面的闭包
以前写 Java 的时候,听到前端同学谈论闭包,觉得甚是新奇,后面自己写了一小段时间 JS,虽只学到皮毛,也大概了解到闭包的概念,现在工作常用语言是 Go,很多优雅的代码中总是有闭包的身影,看来不了解个透是不可能的了,本文让我来科普(按照自己水平随便瞎扯)一下:
1、什么是闭包?
在真正讲述闭包之前,我们先铺垫一点知识点:
- 函数式编程
- 函数作用域
- 作用域的继承关系
1.1 前提知识铺垫
1.2.1 函数式编程
函数式编程是一种编程范式,看待问题的一种方式,每一个函数都是为了用小函数组织成为更大的函数,函数的参数也是函数,函数返回的也是函数。我们常见的编程范式有:
- 命令式编程:
- 主要思想为:关注计算机执行的步骤,也就是一步一步告诉计算机先做什么再做什么。
- 先把解决问题步骤规范化,抽象为某种算法,然后编写具体的算法去实现,一般只要支持过程化编程范式的语言,我们都可以称为过程化编程语言,比如 BASIC,C 等。
- 声明式编程:
- 主要思想为:告诉计算机应该做什么,但是不指定具体要怎么做,比如 SQL,网页编程的 HTML,CSS。
- 函数式编程:
- 只关注做什么而不关注怎么做,有一丝丝声明式编程的影子,但是更加侧重于”函数是第一位“的原则,也就是函数可以出现在任何地方,参数、变量、返回值等等。
函数式编程可以认为是面向对象编程的对立面,一般只有一些编程语言会强调一种特定的编程方式,大多数的语言都是多范式语言,可以支持多种不同的编程方式,比如 JavaScript ,Go 等。
函数式编程是一种思维方式,将电脑运算视为函数的计算,是一种写代码的方法论,其实我应该聊函数式编程,然后再聊到闭包,因为闭包本身就是函数式编程里面的一个特点之一。
在函数式编程中,函数是头等对象,意思是说一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。(维基百科)
一般纯函数编程语言是不允许直接使用程序状态以及可变对象的,函数式编程本身就是要避免使用 共享状态,可变状态,尽可能避免产生 副作用。
函数式编程一般具有以下特点:
函数是第一等公民:函数的地位放在第一位,可以作为参数,可以赋值,可以传递,可以当做返回值。
没有副作用:函数要保持纯粹独立,不能修改外部变量的值,不修改外部状态。
引用透明:函数运行不依赖外部变量或者状态,相同的输入参数,任何情况,所得到的返回值都应该是一样的。
1.2.2 函数作用域
作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
通俗易懂的说,函数作用域是指函数可以起作用的范围。函数有点像盒子,一层套一层,作用域我们可以理解为是个封闭的盒子,也就是函数的局部变量,只能在盒子内部使用,成为独立作用域。
函数内的局部变量,出了函数就跳出了作用域,找不到该变量。(里层函数可以使用外层函数的局部变量,因为外层函数的作用域包括了里层函数),比如下面的 innerTmep
出了函数作用域就找不到该变量,但是 outerTemp
在内层函数里面还是可以使用。
不管是任何语言,基本存在一定的内存回收机制,也就是回收用不到的内存空间,回收的机制一般和上面说的函数的作用域是相关的,局部变量出了其作用域,就有可能被回收,如果还被引用着,那么就不会被回收。
1.2.3 作用域的继承关系
所谓作用域继承,就是前面说的小盒子可以继承外层大盒子的作用域,在小盒子可以直接取出大盒子的东西,但是大盒子不能取出小盒子的东西,除非发生了逃逸(逃逸可以理解为小盒子的东西给出了引用,大盒子拿到就可以使用)。一般而言,变量的作用域有以下两种:
全局作用域:作用于任何地方
局部作用域:一般是代码块,函数、包内,函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部
1.2 闭包的定义
“多数情况下我们并不是先理解后定义,而是先定义后理解“,先下定义,读不懂没关系:
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。 换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。 闭包会随着函数的创建而被同时创建。
一句话表述:
$$
闭包 = 函数 + 引用环境
$$
以上定义找不到 Go语言 这几个字眼,聪明的同学肯定知道,闭包是和语言无关的,不是 JavaScript 特有的,也不是 Go 特有的,而是函数式编程语言的特有的,是的,你没有看错,任何支持函数式编程的语言都支持闭包,Go 和 JavaScript 就是其中之二, 目前 Java 目前版本也是支持闭包的,但是有些人可能认为不是完美的闭包,详细情况文中讨论。
1.3 闭包的写法
1.3.1 初看闭包
下面是一段闭包的代码:
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
}
输出的结果:
先获取函数,不求结果
等待一会
求结果...
结果: 15
可以看出,里面的 sum()
方法可以引用外部函数 lazySum()
的参数以及局部变量,在lazySum()
返回函数 sum()
的时候,相关的参数和变量都保存在返回的函数中,可以之后再进行调用。
上面的函数或许还可以更进一步,体现出捆绑函数和其周围的状态,我们加上一个次数 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
}
上面代码输出什么呢?次数 count
会不会发生变化,count
明显是外层函数的局部变量,但是在内存函数引用(捆绑),内层函数被暴露出去了,执行结果如下:
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
第 2 次求结果...
结果: 15
第 3 次求结果...
结果: 15
结果是 count
其实每次都会变化,这种情况总结一下:
- 函数体内嵌套了另外一个函数,并且返回值是一个函数。
- 内层函数被暴露出去,被外层函数以外的地方引用着,形成了闭包。
此时有人可能有疑问了,前面是lazySum()
被创建了 1 次,执行了 3 次,但是如果是 3 次执行都是不同的创建,会是怎么样呢?实验一下:
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
}
执行的结果如下,每次执行都是第 1 次:
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
从以上的执行结果可以看出:
闭包被创建的时候,引用的外部变量count
就已经被创建了 1 份,也就是各自调用是没有关系的。
继续抛出一个问题,如果一个函数返回了两个函数,这是一个闭包还是两个闭包呢?下面我们实践一下:
一次返回两个函数,一个用于计算加和的结果,一个计算乘积:
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
}
运行结果如下:
先获取函数,不求结果
等待一会
第 1 次求加和...
结果: 15
第 2 次求乘积...
结果: 0
从上面结果可以看出,闭包是函数返回函数的时候,不管多少个返回值(函数),都是一次闭包,如果返回的函数有使用外部函数变量,则会绑定到一起,相互影响:
闭包绑定了周围的状态,我理解此时的函数就拥有了状态,让函数具有了对象所有的能力,函数具有了状态。
1.3.2 闭包中的指针和值
上面的例子,我们闭包中用到的都是数值,如果我们传递指针,会是怎么样的呢?
import "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)
}
}
运行结果如下:
test inner i = 1
func inner i = 2
outer i = 2
可以看出如果是指针的话,闭包里面修改了指针对应的地址的值,也会影响闭包外面的值。这个其实很容易理解,Go 里面没有引用传递,只有值传递,那我们传递指针的时候,也是值传递,这里的值是指针的数值(可以理解为地址值)。
当我们函数的参数是指针的时候,参数会拷贝一份这个指针地址,当做参数进行传递,因为本质还是地址,所以内部修改的时候,仍然可以对外部产生影响。
闭包里面的数据其实地址也是一样的,下面的实验可以证明:
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)
}
}
输出如下, 因此可以推断出,闭包如果引用外部环境的指针数据,只是会拷贝一份指针地址数据,而不是拷贝一份真正的数据(先留个问题:拷贝的时机是什么时候呢):
test inner i address 0xc0003fab98
func inner i address 0xc0003fab98
outer i address 0xc0003fab98
1.3.2 闭包延迟化
上面的例子仿佛都在告诉我们,闭包创建的时候,数据就已经拷贝了,但是真的是这样么?
下面是继续前面的实验:
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)
}
}
我们在创建闭包之后,把数据改了,之后执行闭包,答案肯定是真实影响闭包的执行,因为它们都是指针,都是指向同一份数据:
test inner i = 1
outer i before testFunc 101
func inner i = 102
outer i after testFunc 102
假设我们换个写法,让闭包外部环境中的变量在声明闭包函数的之后,进行修改:
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
}
实际执行结果,count
会是修改后的值:
等待一会
第 100 次求结果...
结果: 15
这也证明了,实际上闭包并不会在声明var sum = func() int {...}
这句话之后,就将外部环境的 count
绑定到闭包中,而是在函数返回闭包函数的时候,才绑定的,这就是延迟绑定。
如果还没看明白没关系,我们再来一个例子:
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
以上两个例子,都是闭包延迟绑定的问题导致,这也可以说是 feature,到这里可能不少同学还是对闭包绑定外部变量的时机有疑惑,到底是返回闭包函数的时候绑定的呢?还是真正执行闭包函数的时候才绑定的呢?
下面的例子可以有效的解答:
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 Routine的匿名函数的延迟绑定就是闭包的延迟绑定,上面的例子中,go func(){}
获取到的就是最新的值,而不是原始值0
。
总结一下上面的验证点:
- 闭包每次返回都是一个新的实例,每个实例都有一份自己的环境。
- 同一个实例多次执行,会使用相同的环境。
- 闭包如果逃逸的是指针,会相互影响,因为绑定的是指针,相同指针的内容修改会相互影响。
- 闭包并不是在声明时绑定的值,声明后只是预留了外部环境(逃逸分析),真正执行闭包函数时,会获取最新的外部环境的值(也称为延迟绑定)。
- Go Routine的匿名函数的延迟绑定本质上就是闭包的延迟绑定。
2、闭包的好处与坏处?
2.1 好处
纯函数没有状态,而闭包则是让函数轻松拥有了状态。但是凡事都有两面性,一旦拥有状态,多次调用,可能会出现不一样的结果,就像是前面测试的 case 中一样。那么问题来了:
Q:如果不支持闭包的话,我们想要函数拥有状态,需要怎么做呢?
A: 需要使用全局变量,让所有函数共享同一份变量。
但是我们都知道全局变量有以下的一些特点(在不同的场景,优点会变成缺点):
- 常驻于内存之中,只要程序不停会一直在内存中。
- 污染全局,大家都可以访问,共享的同时不知道谁会改这个变量。
闭包可以一定程度优化这个问题:
- 不需要使用全局变量,外部函数局部变量在闭包的时候会创建一份,生命周期与函数生命周期一致,闭包函数不再被引用的时候,就可以回收了。
- 闭包暴露的局部变量,外界无法直接访问,只能通过函数操作,可以避免滥用。
除了以上的好处,像在 JavaScript 中,没有原生支持私有方法,可以靠闭包来模拟私有方法,因为闭包都有自己的词法环境。
2.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
:
也就是返回函数的时候,实际上返回结构体,结构体里面记录了函数的引用环境。
4、浅聊一下
4.1 Java 支不支持闭包?
网上有很多种看法,实际上 Java 虽然暂时不支持返回函数作为返参,但是Java 本质上还是实现了闭包的概念的,所使用的的方式是内部类的形式,因为是内部类,所以相当于自带了一个引用环境,算是一种不完整的闭包。
目前有一定限制,比如是 final
声明的,或者是明确定义的值,才可以进行传递:
Stack Overflow上有相关答案:https://stackoverflow.com/questions/5443510/closure-in-java-7
4.2 函数式编程的前景怎么样?
下面是Wiki的内容:
函数式编程长期以来在学术界流行,但几乎没有工业应用。造成这种局面的主因是函数式编程常被认为严重耗费CPU和存储器资源[18] ,这是由于在早期实现函数式编程语言时并没有考虑过效率问题,而且面向函数式编程特性,如保证参照透明性等,要求独特的数据结构和算法。[19]
然而,最近几种函数式编程语言已经在商业或工业系统中使用[20],例如:
- Erlang,它由瑞典公司爱立信在20世纪80年代后期开发,最初用于实现容错电信系统。此后,它已在Nortel、Facebook、Électricité de France和WhatsApp等公司作为流行语言创建一系列应用程序。[21][22]
- Scheme,它被用作早期Apple Macintosh计算机上的几个应用程序的基础,并且最近已应用于诸如训练模拟软件和望远镜控制等方向。
- OCaml,它于20世纪90年代中期推出,已经在金融分析,驱动程序验证,工业机器人编程和嵌入式软件静态分析等领域得到了商业应用。
- Haskell,它虽然最初是作为一种研究语言,也已被一系列公司应用于航空航天系统,硬件设计和网络编程等领域。
其他在工业中使用的函数式编程语言包括多范型的Scala[23]、F#,还有Wolfram语言、Common Lisp、Standard ML和Clojure等。
从我个人的看法,不看好纯函数编程,但是函数式编程的思想,我相信以后几乎每门高级编程需要都会具备,特别期待 Java 拥抱函数式编程。从我自己了解的语言看,像 Go,JavaScript 中的函数式编程的特性,都让开发者深爱不已(当然,如果写出了bug,就是深恶痛疾)。
最近突然火了一波的原因,也是因为世界不停的发展,内存也越来越大,这个因素的限制几乎要解放了。
我相信,世界就是绚丽多彩的,要是一种事物统治世界,绝无可能,更多的是百家争鸣,编程语言或者编程范式也一样,后续可能有集大成者,最终最终历史会筛选出最终符合人类社会发展的。
【作者简介】:
秦怀,公众号【秦怀杂货店】作者,个人网站:http://aphysia.cn,技术之路不在一时,山高水长,纵使缓慢,驰而不息。
聊聊Go里面的闭包的更多相关文章
- 匹夫细说C#:委托的简化语法,聊聊匿名方法和闭包
0x00 前言 通过上一篇博客<匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和U3D给的>的内容,我们实现了使用委托来构建我们自己的消息系统的过程.但是在日常的开发中,仍然有很多开发者因为 ...
- 聊聊Python中的闭包和装饰器
1. 闭包 首先我们明确一下函数的引用,如下所示: def test1(): print("--- in test1 func----") # 调用函数 test1() # 引用函 ...
- C#语法糖系列 —— 第三篇:聊聊闭包的底层玩法
有朋友好奇为什么将 闭包 归于语法糖,这里简单声明下,C# 中的所有闭包最终都会归结于 类 和 方法,为什么这么说,因为 C# 的基因就已经决定了,如果大家了解 CLR 的话应该知道, C#中的类最终 ...
- js闭包-在你身边却不知
今天组里小伙很纳闷的问了我js绑事件带出的一个小问题,随便聊聊闭包那点事,背景如下: 当点击Button的时候给li绑定事件,事件的大概内容是获取li位置的index再做点事,据他描述代码看上去也没错 ...
- 聊一下JS中的作用域scope和闭包closure
聊一下JS中的作用域scope和闭包closure scope和closure是javascript中两个非常关键的概念,前者JS用多了还比较好理解,closure就不一样了.我就被这个概念困扰了很久 ...
- JavaScript ——闭包理解
昨天晚上听别人谈起闭包这个东西,虽然对js有一点了解但却丝毫没有印象,今天也没什么事就顺便研究了一下满足好奇宝宝.整合于网上的理解,记录一下. 一.闭包的作用域 要理解闭包,首先必须理解Javascr ...
- 直播开始:'云榨汁机'诞生记--聊聊JavaScript中的'业务建模'
闭包是JavaScript中的一个重要特性,在之前的博文中,我们说闭包是一个'看似简单,其实很有内涵'的特性.当我们用JavaScript来实现相对复杂的业务建模时,我们可以如何利用'闭包'这个特性呢 ...
- Java编程之委托代理回调、内部类以及匿名内部类回调(闭包回调)
最近一直在看Java的相关东西,因为我们在iOS开发是,无论是Objective-C还是Swift中,经常会用到委托代理回调,以及Block回调或者说是闭包回调.接下来我们就来看看Java语言中是如何 ...
- 「JavaScript」同步、异步、回调执行顺序之经典闭包setTimeout分析
聊聊同步.异步和回调 同步,异步,回调,我们傻傻分不清楚, 有一天,你找到公司刚来的程序员小T,跟他说:“我们要加个需求,你放下手里的事情优先支持,我会一直等你做完再离开”.小T微笑着答应了,眼角却滑 ...
- 聊聊JavaScript-闭包
今天聊聊闭包,网上五花八门的定义和解释很多很多,是不是搞得你很懵逼:每次看闭包,都不同,本来自己懂,看完别人的之后就开始怀疑自己了.在我看来,闭包简单的说就是函数里面套函数,再往大了说就是我函数外面想 ...
随机推荐
- Go编译过程
一. Go编译流程 二.过程说明 1. 词法解析 读取Go源文件,将字符序列转换为符号(token)序列,比如将":="转换为_Define 代码中的标识符.关键字.运算符和分隔符 ...
- 分布式链路追踪体验-skywalking入门使用
背景 旁友,你的线上服务是不是偶尔来个超时,或者突然抖动一下,造成用户一堆反馈投诉.然后你费了九牛二虎之力,查了一圈圈代码和日志才总算定位到问题原因了.或者公司内部有链路追踪系统,虽然可以很轻松地通过 ...
- Vite+React搭建开发构建环境实践
前言 使用 Vite 已经有两年了,期间使用它开发过单页面应用,也开发过浏览器扩展插件,对比日常工作中用到的 webpack 构建速度大幅提升,开发体验也好很多. 虽然相比于 webpack 来说简单 ...
- 我也是一个“翻译家”——关于“robust”
每次看到"鲁棒性",总是不知道是什么意思,一度怀疑自己是不是中国人,是不是说汉语.每次都要查英汉字典,然后一次次看到: robust(adj.精力充沛的; 坚定的; 粗野的,粗鲁的 ...
- 创建Elasticsearch集群并为它们配置TLS安全通信
文章转载自:https://elasticstack.blog.csdn.net/article/details/105636302 文章开头讲述的是两台es主机构建一个集群,其中有关的配置可以借鉴 ...
- ProxySQL 匹配规则
现实中很多场景要求更新数据能立马查到数据,而主从同步在这方面无解,所以从规则上修改,一些需要及时查询的语句在主上. # 用户登录 mysql -h192.168.0.103 -P16032 -urad ...
- Prometheus 通过 consul 分布式集群实现自动服务发现
转载自:https://cloud.tencent.com/developer/article/1611091 1.Consul 介绍 Consul 是基于 GO 语言开发的开源工具,主要面向分布式, ...
- java基础二、类与继承
员工类 Employee, 经理类:Manager public class Employee { private String name; private double salary; privat ...
- Java线程同步的四种方式详解(建议收藏)
Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...
- PAT (Basic Level) Practice 1014 福尔摩斯的约会 分数 20
大侦探福尔摩斯接到一张奇怪的字条: 我们约会吧! 3485djDkxh4hhGE 2984akDfkkkkggEdsb s&hgsfdk d&Hyscvnm 大侦探很快就明白了,字 ...