Golang | defer\return\panic\recover

2,195 阅读7分钟

一、defer 和 return

1、defer和return的顺序

defer是在return之后执行的,但是在写代码的时候,defer要写在return之前,否则defer在return后不会执行;

defer什么时候会执行:函数执行了return语句、运行到函数结尾自动返回、对应的goroutine panic

if err!=nil{
    return
}
defer func(){
 fmt.Println("defer"
}()
//此处return后,defer不会执行;

defer会将待执行函数加入到一个类似栈的地方,等return之后在来执行这个栈里面的函数;

所以return之后的defer是没在栈里的;

2、defer和带名返回值

如果函数的返回值是无名的,则go语言会在执行return的时候会执行一个类似创建一个临时变量作为保存return值的动作;

对于有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作而后续的defer函数会改变这个返回值(虽然defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值)

func main() {
 fmt.Println("test1: ",test1().Error())
 fmt.Println("test3: ",test3().Error())
    //运行结果
    //test1:   this is test2's error (错了,返回的err不是test1的)
    //test3:   this is test3's error
    
    //总结:defer会对带名返回值产生修改
}

func test1() (err error) {
 defer func() {
 //这里会把原来的err覆盖掉
 err = test2()
 }()
 //运行到某处,报err了,然后return,执行defer
 err = errors.New("this is test1's error")
 return
}

func test2() (err error) {
 err = errors.New("this is test2's error")
 return
}

func test3() error {
 var err error
 defer func() {
 //不会覆盖原来的err
 err = test2()
 }()
 //运行到某处,报err了,然后return,执行defer
 err = errors.New("this is test3's error")
 return err
}
//有名返回值的另一个陷阱
func test6()(err error){
 if true{
        //有时候在多返回值的情况下,使用:=时,会不小心新建一个同名的err变量,就会出现问题了
 err:=errors.New(" this is test6's error")
 fmt.Println(err)
 //return err
 }
 return
}
如果把ifreturn去掉,那么上层得到的只是一个nil,因为if内的err是一个新的变量,和返回值没关系;
所以为了将err返回给上层,要在if里直接return err;
或者:
err1:=errors.New(" this is test6's error")
err = err1
3、defer执行匿名函数
func main() {
 test4()
 test5()
 //运行结果:
 //num =  20
 //num =  10
}
func test4(){
 num := 10
 defer func(){
 //这里的num相当于全局变量,当num被修改时,再来执行defer,使用的num值是已经被修改过了的
 fmt.Println("num = ",num)   
 }()
 num = 20
 return
}
func test5(){
 num := 10
 defer func(n int){
 //这里通过函数参数来接收num,能够保存num的值,当num被修改后再来执行defer,还是能够拿到最开始的num值
 fmt.Println("num = ",n)
 }(num)
 num = 20
 return
}
4、返回值被隐藏
func test(i int) (err error) {
 if i == 0 {
 //正常,这里新声明了一个err,但是return时是给返回值的err赋值
 //err := fmt.Errorf("the i is equal 0")
 //return err

 //正常,这是直接给返回值的err赋值
 //err = fmt.Errorf("the i is equal 0")
 //return

 //错误,err is shadowed during return(返回时err被隐藏)
    //这里新声明了一个err,所以return时,返回值的err仍然是空的;提示err被隐藏;
    //不止是error类型会,其他类型也会;
 err := fmt.Errorf("the i is equal 0")
 fmt.Print(err)
 return
 }
 return
}
//之所以这样是因为出现了局部变量和返回值同名的情况,且return时是直接return,这样局部变量就被隐藏了;
//所以,在多返回值且使用短声明时,注意是否会声明新的和返回值用名的变量
//如:result,err:=test()
//那么什么时候,多返回值会不小心声明了新的变量?
//这和变量的作用域有关,如果在新的作用域里,很可能就会产生新的变量
func test(i int) ( err error) {
    //这里的err是返回值的err,编译正常。因为是同个作用域
 j ,err:= test1() 
 fmt.Println(j,err)
    
 if i == 0 {
    //if内部是新的作用域,所以这里的err是新的变量了。编译会报错,err is shadowed during return
 j ,err:= test1() 
 fmt.Println(j,err)
 return
 }
 return
}
5、变量作用域
  • 全局变量: 定义:函数外部定义的变量都属于全局变量;全局变量声明必须以 var 开头 生效范围:当前 package 内,如果想要其他 package 访问,全局变量以大写开头
  • 参数变量: 定义:函数调用时传递的变量 生效范围:函数范围(但是有可能会可变传入参数的值,取决于传递的参数的类型)
  • 局部变量: 定义:在函数或者在语义块(if/for等)中定义的变量 生效范围:定义在函数里面则在整个函数范围有效; 定义在语义块,则在整个语义块生效
//作用域
函数内,if内,for内,都是新的作用域
注意作用域的优先级(当变量名相同时,会优先使用当前作用域的变量)
func Test2(){
    str := "local var"
    if true {
        str := "block var"
        fmt.Println(str) //优先使用当前作用域的变量
    }
    fmt.Println(str)
}

//参数变量和局部变量
两者区别:参数变量的赋值是在函数外部完成的(调用函数时);参数变量声明后可以不使用,局部变量声明不使用编译错误
大部分情况下,参数变量和局部本地变量是一样的

//全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑;
//函数的参数、reciever、返回值都拥有当前函数的作用域;

二、panic和recover

  • panic

    • panic会停止当前goroutine的正常执行。
    • 当函数F调用panic时,函数F被立即停止,然后运行所有在F函数中的defer函数,然后F返回到调用他的函数。
    • 对于调用者G,F函数的行为就像panic一样,会终止G的执行并运行G中所defer函数,然后继续返回,此过程会持续到当前goroutine的最顶层,然后当前goroutine异常退出。
    • panic可以通过内置的recover来捕获。
  • recover

    • recover用来管理含有panic行为的goroutine,recover运行在defer函数中,获取panic抛出的错误值,并将程序恢复成正常执行的状态。
    • 如果在defer函数之外调用recover,那么recover不会停止并且捕获panic错误。
    • 如果goroutine中没有panic,那么recover的返回值也是nil。由此可见,recover的返回值表示当前goroutine是否有panic行为
  • 注意:

    • 如果一直没有recover,抛出的panic到当前goroutine最上层函数时,协程直接异常终止。
    • recover都是在当前的goroutine里进行捕获的,这就是说,对于创建goroutine的外层函数,如果goroutine内部发生panic并且内部没有用recover,外层函数是无法用recover来捕获的,这样会造成整个程序崩溃。 注意:协程panic了,会导致整个程序崩溃。我写HandleFunc时,并没有写recover啊,为什么有时候panic程序不会崩溃?因为http的底层帮你recover了;
    • recover返回的是interface{}类型而不是go中的 error 类型
func main() {
 fmt.Println("我是main")
 go test()
 time.Sleep(5 * time.Second)
 fmt.Println("我是main")
}

func test() {
 defer func() {
  err := recover()
  if err != nil {
   fmt.Println("捕捉到panic:",err)
  }
 }()
 fmt.Println("我是协程")
 time.Sleep(3 * time.Second)
 panic("test_panic!")
}

本文使用 mdnice 排版