5. 闭包
1. 闭包
1.1 概念
闭包是由函数及其相关引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
闭
包
=
函
数
+
引
用
环
境
闭包=函数+引用环境
闭包=函数+引用环境
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):
(1)多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存。
(2)用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。
package main
func fa(a int) func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
//f引用的外部的闭包环境包括本次函数调用的形参a的值1
f := fa(1)
//g引用的外部的闭包环境包括本次函数调用的形参a的值1
g := fa(1)
// 此时f、g引用的闭包环境中的a的值并不是同一个,而是两次函数调用产生的副本
println(f(1))
//多次调用f引用的是同一个副本a
println(f(1))
//g中a的值仍然是1
println(g(1))
println(g(1))
}
- 如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。
- 同一个函数返回的多个闭包共享该函数的局部变量。
package main
func fa(base int) (func(int) int, func(int) int) {
println(&base, base)
add := func(i int) int {
base += 1
println(&base, base)
return base
}
sub := func(i int) int {
base -= 1
println(&base, base)
return base
}
return add, sub
}
func main() {
// f、g闭包引用的base是同一个,是fa函数调用传递过来的实参值
f, g := fa(0)
// s、k闭包引用的base是同一个,是fa函数调用传递过来的实参值
s, k := fa(0)
//f、g和s、k引用不同的闭包变量,这是由于fa每次调用都要重新分配形参
println(f(1), g(2))
println(s(1), k(2))
}
1.2 闭包的价值
对象是附有行为的数据,而闭包是附有数据的行为
2. panic和recover
- panic和recover两个内置函数是用来处理Go的运行时错误(runtime errors)。
- panic用来主动抛出错误
- recover用来捕获panic抛出的错误。
2.1 基本概念
panic和recover的函数签名如下:
panic(i interface{})
recover()interface{}
- 引发panic有两种情况:一种是程序主动调用panic函数,另一种是程序产生运行时错误,由运行时检测并抛出。
- 发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,直到被recover捕获或运行到最外层函数而退出。
- panic的参数是一个空接口类型interface{},所以任意类型的变量都可以传递给panic,调用panic的方法:
panic(xxx)
- panic不但可以在函数正常流程中抛出,在defer逻辑里也可以再次调用panic或抛出panic,defer里面的panic能够被后续执行的defer捕获。
- recover()用来捕获panic,阻止panic继续向上传递。recover()和defer一起使用,但是recover()只有在defer后面的函数内被直接调用才能捕获panic终止异常,否则返回nil,异常继续向外传递。
- 可以有连续多个panic被抛出,连续多个panic的场景只能出现在延迟调用里面,否则不会出现多个panic被抛出的场景。但只有最后一次panic能被捕获。
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
// 只有最后一次panic调用能够被捕获
defer func() {
panic("first defer panic")
}()
defer func() {
panic("second defer panic")
}()
defer func() {
panic("second defer panic")
}()
}
- 包含init函数引发的panic只能在init函数中捕获,在main中无法被捕获,原因是init函数先于main执行。
- 函数并不能捕获内部新启动的goroutine所抛出的panic
2.2 使用场景
主动调用panic函数抛出panic,一般有两种场景:
(1)程序遇到了无法正常执行下去的错误,主动调用panic函数结束程序运行
(2)在调试程序时,通过主动调用panic实现快速退出,panic打印出的堆栈能够更快地定位错误。
为了保证程序的健壮性,需要主动在程序的分支流程上使用recover()拦截运行时错误。
Go提供了两种处理错误的方式,一种时借助panic和recover的抛出捕获极值,另一种时使用error错误类型。
3. 错误处理
3.1 error
- Go语言内置错误接口类型error。任何类型只要实现Error() string方法,都可以传递error接口类型变量。Go语言典型的错误处理方式是将error作为函数最后一个返回值。在调用函数时,通过检测其返回的error值是否为nil来进行错误处理。
type error interface{
Error() string
}
- Go语言标准库提供了两个函数返回实现了error接口的具体类型实例,一般的错误可以使用这两个函数进行封装。遇到复杂的错误,用户也可以自定义错误类型,只要其实现error接口即可。
import "errors"
// Errorf formats according to a format specifier and returns the string as a
// value that satisfies error.
//
// If the format specifier includes a %w verb with an error operand,
// the returned error will implement an Unwrap method returning the operand. It is
// invalid to include more than one %w verb or to supply it with an operand
// that does not implement the error interface. The %w verb is otherwise
// a synonym for %v.
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
//package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
- 处理错误的最佳实践:
- 在多个返回值的函数中,error通常作为函数最后一个返回值。
- 如果一个函数返回error类型变量,则先用if语句处理error!=nil的异常场景,正常逻辑放到if语句块的后面,保持代码平坦
- defer语句应该放到err判断的后面,不然有可能产生panic
- 在错误逐级向上传递过程中,错误信息应该不断地丰富和完善,而不是简单地抛出下层调用的错误。
3.2 错误和异常
- Go是一门类型安全的语言,其运行时不会出现编译器和运行时都无法捕获的错误,即Go语言不存在所谓的异常,出现的"异常"全是错误。
- Go程序需要处理的这些错误可以分为两类:
- 一类是运行时错误(runtime errors),此类错误语言的运行时能够捕获,并采取措施——隐式或显式地抛出panic
- 一类是程序逻辑错误:程序执行结果不符合预期,但不会引发运行时错误。
Go对于错误提供了两种处理机制:
(1)通过函数返回错误类型的值来处理错误
(2)通过panic打印程序调用栈,终止程序执行来处理错误
- 程序发生的错误导致程序不能容错继续执行,此时程序应该主动调用panic或由运行时抛出panic
- 程序虽然发生错误,但是程序能够容错继续执行,此时应该使用错误返回值的方式处理错误,或者在可能发生运行时错误的非关键分支上使用recover捕获panic。
4. 底层实现
研究底层实现有两种办法:一种是看语言编译器源码,分析其对函数的各个特性的处理逻辑,另一种是反汇编,将可执行程序反汇编出来。
4.1 函数调用规约
Go函数使用的是caller-save的模式,即由调用者负责保存寄存器,所以在函数的头尾不会出现push ebp; mov esp ebp这样的代码,相反其是在主调函数调用被调函数的前后有一个保存现场和恢复现场的动作。
//开辟栈空间,压栈 BP 保存现场
SUBQ $x, SP //为函数开辟裁空间
MOVQ BP, y(SP) //保存当前函数 BP 到 y(SP)位直, y 为相对 SP 的偏移量
LEAQ y(SP), BP //重直 BP,使其指向刚刚保存 BP 旧值的位置,这里主要
//是方便后续 BP 的恢复
//弹出栈,恢复 BP
MOVQ y(SP), BP //恢复 BP 的值为调用前的值
ADDQ $x, SP //恢复 SP 的值为函数开始时的位
Go 编译器产生的汇编代码是一种中间抽象态,它不是对机器码的映射,而是和平台无关的一个中间态汇编描述,所以汇编代码中有些寄存器是真实的,有些是抽象的,几个抽象的寄存器如下:
- SB (Static base pointer):静态基址寄存器,它和全局符号一起表示全局变量的地址。
- FP (Frame pointer):栈帧寄存器,该寄存器指向当前函数调用栈帧的栈底位置。
- PC (Program counter):程序计数器,存放下一条指令的执行地址,很少直接操作该寄存器,一般是 CALL、RET 等指令隐式的操作。
- SP (Stack pointer):栈顶寄存器,一般在函数调用前由主调函数设置 SP 的值对栈空间进行分配或回收。
4.2 多值返回分析
多值返回函数 swap 的源码如下:
package main
func swap(a, b int) (x int, y int) {
x = b
y = a
return
}
func main() {
swap(10, 20)
}
编译生成汇编如下:
//- S 产生汇编的代码
//- N 禁用优化
//- 1 禁用内联
GOOS=linux GOARCH=amd64 go tool compile -1 -N -S swap.go >swap.s 2>&1
汇编代码分析:
- swap 函数和 main 函数汇编代码分析。例如:
1"".swap STEXT nosplit size=39 args=0x20 locals=0x0
2 0x0000 00000 (swap.go:4) TEXT "".swap(SB), NOSPLIT, $0 - 32
3 0x0000 00000 (swap.go:4) FUNCDATA $0, gclocals.ff19ed39bdde8a01a800918ac3ef0ec7(SB)
4 0x0000 00000 (swap.go:4) FUNCDATA $1, gclocals.33cdeccccebe80329flfdbee7f5874cb(SB)
5 0x0000 00000 (swap.go:4) MOVQ $0, "".x+24(SP)
6 0x0009 00009 (swap.go:4) MOVQ $0, "".y+32(SP)
7 0x0012 00018 (swap.go:5) MOVQ "".b+16(SP), AX
8 0x0017 00023 (swap.go:5) MOVQ AX, "".x+24(SP)
9 0xOO1c 00028 (swap.go:6) MOVQ "".a+8(SP), AX
10 0x0021 00033 (swap.go:6) MOVQ AX, "".y+32(SP)
11 0x0026 00038 (swap.go:7) RET
12
13
14
15"".main STEXT size=68 args=0x0 locals=0x28
16 0x0000 00000 (swap.go:10) TEXT "".main(SB), $40 - 0
17 0x0000 00000 (swap.go:10) MOVQ (TLS), CX
18 0x0009 00009 (swap.go:10) CMPQ SP, 16(CX)
19 0x000d 00013 (swap.go:10) JLS 61
20 0x000f 00015 (swap.go:10) SUBQ $40, SP
21 0x0013 00019 (swap.go:10) MOVQ BP, 32 (SP)
22 0x0018 00024 (swap.go:10) LEAQ 32(SP), BP
23 0x001d 00029 (swap.go:10) FUNCDATA $0, gclocals ·33cdeccccebe80329flfdbee7f5874cb(SB)
24 0x001d 00029 (swap.go:10) FUNCDATA $1, gclocals ·33cdeccccebe80329flfdbee7f5874cb(SB)
25 0x001d 00029 (swap.go:11) MOVQ $10, (SP)
26 0x0025 00037 (swap.go:11) MOVQ $20 , 8 (SP)
27 0x002e 00046 (swap.go:11) PCDATA $0 , $0
28 0x002e 00046 (swap.go:11) CALL "". swap(SB)
29 0x0033 00051 (swap.go:12) MOVQ 32(SP), BP
30 0x0038 00056 (swap.go:12) ADDQ $40, SP
31 0x003c 00060 (swap.go:12) RET
32 0x003d 00061 (swap.go:12) NOP
33 0x003d 00061 (swap.go:10) PCDATA $0, $ - 1
第 5 行初始化返回值 x 为 0。
第 6 行初始化返回值 y 为 0。
第 7~8 行取第 2 个参数赋值给返回值 x。
第 9~10 行取第 1 个参数赋值给返回值 y。
第 11 行函数返回,同时进行栈回收,FUNCDATA 和垃圾收集可以忽略。
第 15~24 行 main 函数堆栈初始化:开辟栈空间,保存 BP 寄存器。
第 25 行初始化 add 函数的调用参数 1 的值为 10。
第 26 行初始化 add 函数的调用参数 2 的值为 20。
第 28 行调用 swap 函数,注意 call 隐含一个将 swap 下一条指令地址压栈的动作,即 sp=sp+8。
所以可以看到在 swap 里面的所有变量的相对位置都发生了变化,都在原来的地址上 +8。
第 29~30 行恢复栈空间。
从汇编的代码得知:
- 函数的调用者负责环境准备,包括为参数和返回值开辟栈空间。
- 寄存器的保存和恢复也由调用方负责。
- 函数调用后回收栈空间,恢复 BP 也由主调函数负责。
函数的多值返回实质上是在栈上开辟多个地址分别存放返回值,这个并没有什么特别的地方,如果返回值是存放到堆上的,则多了一个复制的动作。
main 调用 swap 函数栈的结构如下图所示:
函数调用前己经为返回值和参数分配了栈空间,分配顺序是从右向左的,先是返回值,然后是参数,通用的栈模型如下:
+----------+
| 返回值 y |
|------------|
| 返回值 x |
|------------|
| 参数 b |
|------------|
| 参数 a |
+----------+
函数的多返回值是主调函数预先分配好空间来存放返回值,被调函数执行时将返回值复制到该返回位置来实现的。
4.3 闭包底层实现
示例程序如下:
package main
func a(i int) func() {
return func() {
print(i)
}
}
func main() {
f := a(1)
f()
}
编译汇编如下:
GOOS=linux GOARCH=amd64 go tool compile -S c2_7_4a.go >c2_7_4a.s 2&1
关键汇编代码及分析如下:
// 函数a对应的汇编代码
1"".a STEXT size=91 args=0x10 locals=0x18
2 0x0000 00000 (c2_7_4a.go:3) TEXT "".a(SB), $24-16
3 0x0000 00000 (c2_7_4a.go:3) MOVQ (TLS), CX
4 0x0009 00009 (c2_7_4a.go:3) CMPQ SP, 16(CX)
5 0x000d 00013 (c2_7_4a.go:3) JLS 84
6 0x000f 00015 (c2_7_4a.go:3) SUBQ $24, SP
7 0x0013 00019 (c2_7_4a.go:3) MOVQ BP , 16(SP)
8 0x0018 00024 (c2_7_4a.go:3) LEAQ 16(SP), BP
9 0x001d 00029 (c2_7_4a.go:3) FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
10 0x001d 00029 (c2_7_4a.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329flfdbee7f5874cb (SB)
11 0x001d 00029 (c2_7_4a.go:4) LEAQ type.noalg.struct{ F uintptr; "".i int}(SB), AX
12 0x0024 00036 (c2_7_4a.go:4) MOVQ AX, (SP)
13 0x0028 00040 (c2_7_4a.go:4) PCDATA $0, $0
14 0x0028 00040 (c2_7_4a.go:4) CALL runtime.newobject(SB)
15 0x002d 00045 (c2_7_4a.go:4) MOVQ 8(SP), AX
16 0x0032 00050 (c2_7_4a.go:4) LEAQ "".a.funcl(SB), CX
17 0x0039 00057 (c2_7_4a.go:4) MOVQ CX, (AX)
18 0x003c 00060 (c2_7_4a.go:3) MOVQ "".i+32(SP), CX
19 0x0041 00065 (c2_7_4a.go:4) MOVQ CX, 8(AX)
20 0x0045 00069 (c2_7_4a.go:4) MOVQ AX, "".~r1+40(SP)
21 0x004a 00074 (c2_7_4a.go:4) MOVQ 16(SP), BP
22 0x004f 00079 (c2_7_4a.go:4) ADDQ $24, SP
// main函数对应的汇编代码
23"".main STEXT size=69 args=0x0 locals=0x18
24 0x0000 00000 (c2_7_4a.go:9) TEXT "".main(SB), $24-0
25 0x0000 00000 (c2_7_4a.go:9) MOVQ (TLS), CX
26 0x0009 00009 (c2_7_4a.go:9) CMPQ SP, 16(CX)
27 0x000d 00013 (c2_7_4a.go:9) JLS 62
28 0x000f 00015 (c2_7_4a.go:9) SUBQ $24, SP
29 0x0013 00019 (c2_7_4a.go:9) MOVQ BP, 16(SP)
30 0x0018 00024 (c2_7_4a.go:9) LEAQ 16(SP), BP
31 0x00ld 00029 (c2_7_4a.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)
32 0x00ld 00029 (c2_7_4a.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329flfdbee7f5874cb(SB)
33 0x00ld 00029 (c2_7_4a.go:10) MOVQ $1, (SP)
34 0x0025 00037 (c2_7_4a.go:10) PCDATA $0, $0
35 0x0025 00037 (c2_7_4a.go:10) CALL "".a(SB)
36 0x002a 00042 (c2_7_4a.go:10) MOVQ 8(SP), DX
37 0x002f 00047 (c2_7_4a.go:11) MOVQ (DX), AX
38 0x0032 00050 (c2_7_4a.go:11) PCDATA $0, $0
39 0x0032 00050 (c2_7_4a.go:11) CALL AX
40 0x0034 00052 (c2_7_4a.go:15) MOVQ 16(SP), BP
41 0x0039 00057 (c2_7_4a.go:15) ADDQ $24, SP
42 0x003d 00061 (c2_7_4a.go:15) RET
func a() 函数分析:
- 第 1~10 行环境准备。
- 第 11 行这里我们看到
type.noalg.struct { F uintptr; "".i int }(SB)
这个符号是一个闭包类型的数据,闭包类型的数据结构如下:
type Closure struct {
F uintptr
i int
}
闭包的结构很简单,一个是函数指针,另一个是对外部环境的引用。注意,这里仅仅是打印 i,并没有修改 i,Go语言编译器并没有传递地址而是传递值。
- 第 11 行将闭包类型元信息放到 (SP) 位置,(SP) 地址存放的是 CALL 函数调用的第一个参数。
- 第 14 行创建闭包对象,我们来看一下
runtime.newobject
的函数原型,该函数的输入参数是一个类型信息,返回值是根据该类型信息构造出来的对象地址。
// src/runtime/malloc.go
func newobject(typ *_type) unsafe.Pointer
- 第 15 行将 newobject 返回的对象地址复制给 AX 寄存器。
- 第 16 行将 a 函数里面的匿名函数 a.func 指针复制到 CX 寄存器。
- 第 17 行将 CX 寄存器中存放的 a.func 函数指针复制到闭包对象的函数指针位置。
- 第 18、19 行将外部闭包变量 i 的值复制到闭包对象的 i 处。
- 第 20 行复制闭包对象指针值到函数返回值位置 “”.~r1+40(SP)。
main() 函数分析: - 第 23~32 行准备环境。
- 第 33 行将立即数 1 复制到 (SP) 位置,为后续的 CALL 指令准备参数。
- 第 35 行调用函数 a()。
- 第 36 行复制函数返回值到 DX 寄存器。
- 第 37 行间接寻址,复制闭包对象中的函数指针到 AX 寄存器。
- 第 39 行调用 AX 寄存器指向的函数。
- 第 40~42 行恢复环境,并返回。
通过汇编代码的分析,清楚地看到 Go 实现闭包是通过返回一个如下的结构来实现的:
type Closure struct {
F uintptr
env *Type
}
**F 是返回的匿名函数指针,env 是对外部环境变量的引用集合,**如果闭包内没有修改外部变量,则 Go 编译器直接优化为值传递,如上面的例子中的代码所示,反之则是通过指针传递的。
至此,我们已经了解了什么是闭包以及闭包的底层实现,下一节我们将学习Go语言中的类型系统。