1,闭包与匿名函数
闭包是匿名函数与匿名函数所引用环境的组合,即 闭包=匿名函数+引用环境,具体如何理解呢?先从例子开始
例1: func squares() func() int { //squares的返回值是一个函数类型的变量,该函数类型是func() int var x int return func() int { //在函数内部创建一个匿名函数,将这个函数作为返回值返回给上层 x++ return x * x } } f := squares() //执行squares函数得到一个函数体
fmt.Println(f()) // 执行squares给创建的函数,得到结果为"1"
fmt.Println(f()) // 再次执行,得到结果"4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
小小结:以上的方式就是闭包技术(closures),首先需要创建匿名函数,然后该匿名函数引用了外层函数的变量x(即引用的环境)
var funcArray []func()
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ { j:=i //j是在循环中定义的 funcArray = append(funcArray, func{print(i)}) funcArray = append(funcArray, func{print(i)}) } }
funcArray[0]() //结果:3 //结果:1
funcArray[1]() //结果:3 //结果:2
funcArray[2]() //结果:3 //结果:3
小小结:第一个,闭包引用的环境是外层函数的变量i,当执行完循环之后,变量i的值为3,于是再执行闭包时,得到的结果就是3
第二个,闭包引用的环境是外层函数的变量j,不同的闭包得到的是不同的j(因为j是在循环中定义的)。
注意,如果j也是在循环外定义的,然后循环中是 j=i,则最终的结果是 222,想想为什么呢?
小结:
闭包有什么好处?应用场景是啥?
a,可以通过闭包将函数内部的局部变量暴露出来
b,闭包的记忆效应:闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应,此时的变量就像全局变量一般的存在。
c, 让某些变量的值始终保持在内存中,只要闭包的引用还在,就不会在调用外层函数结束后,变量被被垃圾回收(garbage collection)。
2,可变参数函数与切片
func sum(vals ...int) int{
total := 0
for _, val := range vals{
total += val
}
return total
}
sum()
sum(3)
sum(1,2,3,4)
var values []int{10,20,30}
sum(values...)
小小结:...类型:可以看做是一种复合类型,表示一个type类型的切片(包括底层的数组),一堆入参进来后,将这些参数复制到数组中,于是可以利用遍历的方式进行访问
变量...: 首先变量必须是某种类型的切片(当然一定要有底层对应的数组),然后 变量...就表示多个参数,每一个参数都是数组中的值
即, ...类型 和 变量... 两种形式,正式一对!
2,defer的运用(闭包 + defer)
func double(x int)(result int){
0)方式零: defer fmt.Println("defer:", x) //defer的是一个语句,不是函数(闭包)
1)方式一: defer func(){result += x}() //首先构造闭包(引用了环境x和result),闭包的引用也在函数内,在result之前将环境中的result和x相加
2)方式二: defer func(i int){result += i}(x) //闭包只引用了环境变量result,同样闭包的引用在函数内(所以要在闭包定义后用 闭包体(入参) 的格式调用)
x++
return x
}
0)double(10) //结果为10。在执行到defer的时候,语句入栈,此时x的值已经固定
1)double(10) //结果为22。return之时,x的值为11,result等于x的值也是11,之后再执行闭包就变成了22
2)double(10) //结果为21. 在执行到defer语句的时候,闭包加载,此时入参i的值已经确定,即外层的x(10),return之时,i为10,result是11,执行闭包的结果就是21
小小结:return不是一个原子指令,包括 给返回值值赋值 和 真正ret 2步,defer的内容(语句/闭包)的执行被放在二者之间
被defer的函数称为defered函数,整个的执行原理是:
1)根据defer关键字,将defered函数入栈,如果有多个defer语句的话,则递归依次入栈
2)等到执行到return语句时,defered函数依次出栈,执行
3)关于入参的值,是在入栈的时候已经确定
4)如果闭包中引入了外部变量,则原理同一般的闭包
被defer的语句同理,只是如果引用了某个变量,则入栈的值就是当前值
2,panic和recover
一班而言,当程序panic异常了,程序会中断,并立刻执行该gouroutine(main所在的也是一个goroutine)中被defered的内容,即依次出栈
随后,程序崩溃系统给出日志信息,包括panic value和堆栈跟踪信息;
有了recover,他会让recover的函数不继续,但正常返回,这样但是整个goroutine还在
func f(x int){ defer func(){ if recover() != nil{fmt.Println("before exec defer:recover after panic")} }() fmt.Printf("f(%d) ", x + 0/x) f(x-1) } func PrintStack() { var buf [4096]byte n := runtime.Stack(buf[:],false) //为什么一定要buf[:] os.Stdout.Write(buf[:n]) } 执行: defer PrintStack() f(1)
结果:
f(1)
goroutine 1 [running]: ##这一坨的错误信息是PrintStack()函数打印的信息=堆栈信息
main.PrintStack() ##根据当前的这个打印可以知道,当panic的时候,处于栈顶的是之前defered函数即PrintStack函数,之后才是panic函数,
C:/Users/HX/go/src/myWork/testFunction.go:54 +0x62
panic(0x4a6ba0, 0x533490) ##我就是panic函数,panic函数下面正是即将引起panic的语句
C:/go1.10/src/runtime/panic.go:505 +0x237
main.f(0x0)
C:/Users/HX/go/src/myWork/testFunction.go:48 +0xde ##是的,我就是真正引起panic的的语句
main.f(0x1)
C:/Users/HX/go/src/myWork/testFunction.go:49 +0xcf
main.testFunction()
C:/Users/HX/go/src/myWork/testFunction.go:60 +0x4d
main.main()
C:/Users/HX/go/src/myWork/main.go:4 +0x29
------------------------------------------------------------
panic: runtime error: integer divide by zero #这一坨为系统直接报出的错误信息=错误原因(panic value) + 堆栈信息
goroutine 1 [running]:
main.f(0x0)
C:/Users/go/src/myWork/testFunction.go:48 +0xde #这一行正是执行0/0的那一杨,所以出错了,于是把目前还在栈中的所有内容打印出来,这里就是栈顶
main.f(0x1)
C:/Users/go/src/myWork/testFunction.go:49 +0xcf
main.testFunction()
C:/Users/go/src/myWork/testFunction.go:60 +0x4d
main.main()
C:/Users/go/src/myWork/main.go:4 +0x29
Process finished with exit code 2
======如果有recover操作,则只会由PrintStack()函数打印堆栈信息,panic不会===================================================================
f(1)
before exec defer:recover after panic #执行了recover操作
goroutine 1 [running]:
main.PrintStack()
C:/Users/go/src/myWork/testFunction.go:54 +0x62 #这里是打印本身的语句,即PrintStack的打印语句
main.testFunction()
C:/Users/go/src/myWork/testFunction.go:75 +0x4e
main.main()
C:/Users/go/src/myWork/main.go:4 +0x29
hello go, I also remember you!hahah! #这里显示程序正常执行下去了,已经到结尾
Process finished with exit code 0
小结:
//runtime/runtime2.go type _panic struct { // 调用defer时入参的指针 argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink arg interface{} // argument to panic link *_panic // link to earlier panic recovered bool // whether this panic is over aborted bool // the panic was aborted }
//runtime/panic.go func gopanic(e interface{}) { gp := getg() ... // 初始化panic var p _panic ... // 遍历 G 的defer链表 for { d := gp._defer ...//调用defer后面的函数。如果函数中包含了recover,那么会调用gorecover reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) ...// 已经有recover被调用 if p.recovered { mcall(recovery) throw("recovery failed") // mcall should not return } } // 终止程序 fatalpanic(gp._panic) // should not return *(*int)(nil) = 0 // not reached }
1,首先要明确,go有运行时即runtime在帮我们运行程序,于是程序执行异常的时候,运行时会替我们调用panic函数,对应到运行时层面的函数就是gopanic
当然,我们可以在用户程序层面直接调用panic,效果一样的
2,然后可以看到,panic函数执行时,会去遍历G的defer列表,于是在异常退出之前先去执行defer函数,这也就是为什么PrintStack函数得以执行,同时此时此刻的堆栈中有defered 函数的信息
3,recover的加入,就是利用了defer的原理,即在退出之前会去执行以下recover,于是recover就得以处理以下堆栈,让当前goroutine不会异常退出,具体来说就是当前的子函数正常return了