GoLang之defer、panic、recover

1.defer关键字

Go 语言中的类没有构造函数和析构函数的概念,处理错误和异常时也没有提供 try...catch...finally 之类的语法,那当我们想要在某个资源使用完毕后将其释放(网络连接、文件句柄等),或者在代码运行过程中抛出错误时执行一段兜底逻辑,要怎么做呢?
通过 defer 关键字声明兜底执行或者释放资源的语句可以轻松解决这个问题
注:defer是一个关键字,ctrl+左键后不会进行包的跳转

如果一条语句干不完清理的工作,也可以在 defer 后加一个匿名函数来执行对应的兜底逻辑:
defer func() {
// 执行复杂的清理工作…
} ()
另外,一个函数/方法中可以存在多个 defer 语句,defer 语句的调用顺序遵循先进后出的原则,即最后一个 defer 语句将最先被执行,相当于「栈」这个数据结构,如果在循环语句中包含了 defer 语句,则对应的 defer 语句执行顺序依然符合先进后出的规则。
由于 defer 语句的执行时机和调用顺序,所以我们要尽量在函数/方法的前面定义它们,以免在后面编写代码时漏掉,尤其是运行时抛出错误会中断后面代码的执行,也就感知不到后面的 defer 语句。

2.defer特性、用途

image-20220123202809611

3.painc内置函数

func panic(v interface{})

注:panic函数是一个内置函数,在Builtin.go里面
前面介绍了 Go 语言通过 error 类型统一进行错误处理,但这些错误都是我们在编写代码时就已经预见并返回的,对于某些运行时错误,比如数组越界、除数为0、空指针引用,这些 Go 语言是怎么处理的呢?
Go 语言没有像 Java、PHP 那样引入异常的概念,也没有提供 try…catch 这样的语法对运行时异常进行捕获和处理,当代码运行时出错,而又没有在编码时显式返回错误时,Go 语言会抛出 panic,中文译作「运行时恐慌」,我们也可以将其看作 Go 语言版的异常

panic 函数支持的参数类型是 interface{}:
func panic(v interface{})
所以可以传入任意类型的参数:
panic(500) // 传入数字
panic(errors.New(“除数不能为0”)) // 传入 error 类型

image-20220125083345829

4.recover内置函数

func recover() interface{}

recover函数是一个内置函数,在Builtin.go里面
我们还可以通过 recover() 函数对 panic 进行捕获和处理,从而避免程序崩溃然后直接退出,而是继续可以执行后续代码,实现类似 Java、PHP 中 try…catch 语句的功能。
由于执行到抛出 panic 的问题代码时,会中断后续其他代码的执行,所以,显然这个 panic 的捕获应该放到 defer 语句中完成,才可以在抛出 panic 时通过 recover 函数将其捕获,defer 语句执行完毕后,会退出抛出 panic 的当前函数,回调调用它的地方继续后续代码的执行。
可以类比为 panic、recover、defer 组合起来实现了传统面向对象编程异常处理的 try…catch…finally 功能。
这样一来,当程序运行过程中抛出 panic 时我们可以通过 recover() 函数对其进行捕获和处理,如果没有抛出则什么也不做,从而确保了代码的健壮性。

5.defer在ReadFile函数内关闭句柄

//比如我们看 Go 内置的 io/ioutil 包提供的读取文件方法 ReadFile 实现源码,其中就有 defer 语句的使用:
func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    var n int64 = bytes.MinRead

    if fi, err := f.Stat(); err == nil {
        if size := fi.Size() + bytes.MinRead; size > n {
            n = size
        }
    }
    return readAll(f, n)
}

6.defer在闭包中的应用(捕获变量被初始化赋值且修改)

image-20220123203132149

7.先return再defer

image-20220125091718500

image-20220123203541168

image-20220125091436237

image-20220123203621904

8.defer在遍历执行结构体方法中的应用

image-20220123204337077
image-20220123204452862

9.defer在遍历执行函数中的应用

image-20220123204420793

10.defer遍历切片输出索引

image-20220123203029302

image-20220123202950124

11.自发panic

//底层抛出 panic
package main

import "fmt"

func main() {
	var i = 1
	var j = 0
	var k = i / j

	fmt.Printf("%d / %d = %d\n", i, j, k)
}
/*                                        
panic: runtime error: integer divide by zero    
goroutine 1 [running]:                          
main.main()                                     
        D:/all project/go/demo/main/1.go:8 +0x11
*/

12.使用defer自定义错误输出在panic之前

//在这段代码中,我们定义了两个 defer 语句,并且是在函数最顶部,以确保异常情况下也能执行。
/*在函数正常执行的情况下,这两个 defer 语句会在最后一条打印语句执行完成后先执行第二条 defer 语句,再执行第一条 defer 语句:
*/
package main

import "fmt"

func printError()  {
    fmt.Println("兜底执行")
}

func main()  {
    defer printError()
    defer func() {
        fmt.Println("除数不能是0!")
    }()

    var i = 1
    var j = 1
    var k = i / j

    fmt.Printf("%d / %d = %d\n", i, j, k)
}
/*输出以下三行:
1 / 1 = 1
除数不能是0!
兜底执行    
*/
//底层抛出 panic
//而如果我们把 j 的值设置为 0,则函数会抛出 panic:
/*表示除数不能为零。这个时候,由于 defer 语句定义在抛出 panic 代码的前面,所以依然会被执行,底层的逻辑是在执行 var k = i / j 这条语句时,遇到除数为 0,则抛出 panic,然后立即中断当前函数 main 的执行(后续其他语句都不再执行),并按照先进后出顺序依次执行已经在当前函数中声明过的 defer 语句,最后打印出 panic 日志及错误信息*/
package main

import "fmt"

func printError() {
	fmt.Println("兜底执行")
}

func main() {
	defer printError()
	defer func() {
		fmt.Println("除数不能是0!")
	}()

	var i = 1
	var j = 0
	var k = i / j

	fmt.Printf("%d / %d = %d\n", i, j, k)
}
/*
除数不能是0!
兜底执行                                         
panic: runtime error: integer divide by zero    
goroutine 1 [running]:                           
main.main()                                      
        D:/all project/go/demo/main/1.go:20 +0x46
*/

13.使用panic

image-20220125083817468

/*没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息,但现在我们在 divide() 函数的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 函数而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行:
 */
package main

import (
	"fmt"
)

func divide() {
	defer func() {
		fmt.Println("222222222222")
	}()
	defer func() {
		fmt.Println("11111111")
	}()
	var i = 1
	var j = 0
	k := i / j
	fmt.Println(k)
}

func main() {
	defer func() {
		fmt.Println("333333333333")
	}()
	divide()
	fmt.Println("divide 方法调用完毕,回到 main 函数")
}

/*
11111111
222222222222
333333333333
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.divide()
        D:/ziliao/goproject/try/main.go:18 +0x46
main.main()
        D:/ziliao/goproject/try/main.go:26 +0x3f
*/

15.使用recover

recove必须搭配defer只用,因为recover是在程序可能出现异常之后才会去执行的
recover函数用来捕捉异常的信息

image-20220125085933322

image-20220125085702011

image-20220125085712441

/*没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息,但现在我们在 divide() 函数的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 函数而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行:
*/
package main

import (
    "fmt"
)

func divide() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("Runtime panic caught: %v\n", err)
        }
    }()

    var i = 1
    var j = 0
    k := i / j
    fmt.Printf("%d / %d = %d\n", i, j, k)
}

func main() {
    divide()
    fmt.Println("divide 方法调用完毕,回到 main 函数")
}
/*输出:
Runtime panic caught: runtime error: integer divide by zero
divide 方法调用完毕,回到 main 函数
*/
/*如果在代码执行过程中没有抛出 panic,比如我们把 divide() 函数中的 j 值改为 1,则代码会正常执行到函数末尾,然后调用 defer 语句声明的匿名函数,此时 recover() 函数返回值为 nil,不会执行 if 分支代码,然后退出 divide() 函数回到 main() 函数执行后续代码*/
package main

import (
	"fmt"
)

func divide() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("Runtime panic caught: %v\n", err)
		}
	}()

	var i = 1
	var j = 1
	k := i / j
	fmt.Printf("%d / %d = %d\n", i, j, k)
}

func main() {
	divide()
	fmt.Println("divide 方法调用完毕,回到 main 函数")
}
/*输出:
1 / 1 = 1
divide 方法调用完毕,回到 main 函数
*/
/*没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息,但现在我们在 divide() 函数的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 函数而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行:
 */
package main

import (
	"fmt"
)

func divide() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("Runtime panic caught: %v\n", err)
		}
	}()

	var i = 1
	var j = 0
	k := i / j
	fmt.Printf("%d / %d = %d\n", i, j, k)
	fmt.Println("111111111111")
	defer func() {
		fmt.Println("111111111111")
	}()
}

func main() {
	divide()
	fmt.Println("divide 方法调用完毕,回到 main 函数")
}

/*输出:
Runtime panic caught: runtime error: integer divide by zero
divide 方法调用完毕,回到 main 函数
*/

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值