Go语言完整解析Go!Go!Go!(一)数据类型 之 函数

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)\n", 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了

          

 

猜你喜欢

转载自www.cnblogs.com/shuiguizi/p/11380635.html
今日推荐