作者:禅与计算机程序设计艺术
1.简介
什么是异常处理?
异常(Exception)是指在运行过程中由于条件不满足、输入数据有误或者其他原因导致的一种不期望的状态。按照定义,异常就是一个对象,表示某个事件或状态发生时产生的信号。一般情况下,系统会抛出各种类型的异常,包括语法错误、运行时异常、逻辑异常等。
对于程序开发者来说,异常处理是一个非常重要的内容,它可以帮助我们更好地处理并记录程序中的错误信息,提高程序的健壮性,减少崩溃的概率。除此之外,异常还可以作为一些控制流机制,如条件判断语句和循环结构中的分支选择,为程序提供了更多的灵活性。因此,掌握异常处理对编程人员来说是至关重要的。
为什么要进行异常处理?
解决问题
- 抓住软件运行中遇到的异常情况,及时止损并向用户反馈;
- 提升软件的鲁棒性,减少因不可预测的外部影响而引发的异常崩溃;
- 使程序的健壮性更好,便于维护和扩展;
- 提升程序员的职业技能。
帮助调试
- 可以帮助定位出现的问题;
- 通过日志和追踪栈信息来分析异常产生原因;
- 输出堆栈信息可以查看程序调用关系、变量值等信息;
- 对比不同异常,可以定位根本原因。
什么时候需要用到异常处理?
- 需要对用户输入的数据做验证,防止恶意攻击等。
- 在多线程环境下,如果某个线程发生异常,可能会造成其他线程无法正常工作。
- 操作数据库时,如果失败了可能需要进行重试或者回滚操作。
- 测试阶段,如果测试案例中的某些步骤产生了异常,则后续的测试将会受到影响。
Go语言如何支持异常处理?
Go语言自带的异常处理方式为panic/recover机制。如果发生了一个未经处理的异常,程序就会终止运行,并进入恐慌模式,此时可以通过panic()函数来触发异常。recover()函数用于从恐慌模式中恢复程序,使其继续运行。
当panic()被调用时,程序会立即停止执行当前函数,并且参数值作为返回值返还给调用者。然后,goroutine会把这个异常抛给调用它的goroutine的监控者,监控者会根据异常类型和其他相关信息来判断是否需要终止整个进程或当前goroutine,还是仅仅让该goroutine结束。具体流程如下图所示:
2.基本概念术语说明
panic
用于引发恐慌(panic),当程序发生运行时错误时,可以调用panic()函数,让程序直接退出,并打印出相应的错误消息。
recover
recover()函数用于恢复panic()引发的异常,使程序能够从异常中恢复过来,以便进一步运行。当程序调用recover()函数时,如果当前goroutine不是在panic状态,那么它会返回nil,否则它会返回panic()函数的参数值。
defer语句
defer语句用来延迟函数调用的执行时间,直到 surrounding function 返回的时候才执行。
goroutine
goroutine 是由 Go 运行时管理的轻量级线程。每一个 goroutine 都有自己的独立栈内存,有利于并发执行。
channel
channel 是Go提供的一种同步机制,用于协调两个或多个goroutine之间的通信。
select语句
select语句用于监听多个channel上的数据流动,类似switch语句用于监听多个io请求。
3.核心算法原理和具体操作步骤以及数学公式讲解
panic函数
func main(){
//...some codes...
if someErrorHappened{
panic("some error message") // 发生错误时调用panic函数,终止程序运行
}
//...some codes...
}
当程序出现异常时,可以使用panic函数终止程序的运行,并打印错误信息。
panic()函数将给定的参数值作为结果返回给调用的goroutine,如果当前的goroutine还没有调用recover()函数,则程序将被终止,所有的goroutine都会停止执行。
recover函数
func main(){
defer func(){
if err := recover();err!= nil {
fmt.Println(err)
}
}()
//...some codes...
// 可能发生异常的代码段
//...some codes...
}
recover()函数只有在被内置的defer函数中才有效。当函数发生panic()时,如果存在recover()函数,它将捕获panic()的输入值,并恢复程序的执行。如果recover()函数不存在,则程序将被终止。
当panic()被调用时,当前的goroutine会终止运行,并把异常抛给监控者。如果recover()函数被调用,则recover()函数可以捕获这个异常,恢复程序的执行,并返回异常的输入值,允许程序继续运行。
捕获多个异常
func main(){
defer func(){
if e:=recover();e!=nil{
switch t:=e.(type){
case runtime.Error:
log.Panicf("%s",t.Error())
case os.PathError:
log.Printf("%v",t)
default:
log.Printf("catch a unknown type of exception:%T,%v\n",t,t)
}
}
}()
//... some codes...
}
为了便于调试和跟踪程序,可以定义多个recover()函数,分别针对不同的异常类型。但是这种方法增加了复杂性,需要编写很多的recover()函数,而且也会降低程序的可读性。
建议使用recover()来捕获所有的异常,同时通过类型断言的方式来处理不同种类的异常。
使用defer函数处理资源释放
package main
import "fmt"
// 定义一个函数作为资源初始化器
func initResources() (err error) {
// 打开文件句柄
file, err = os.OpenFile("/tmp/file.txt", os.O_RDWR|os.O_CREATE, 0755)
return
}
// 定义一个函数作为资源释放器
func freeResources() {
if file == nil {
return
}
file.Close()
}
var file *os.File
func main() {
defer freeResources() // 注册资源释放器
var err error // 声明一个错误变量
if err = initResources(); err!= nil {
fmt.Println("init resource failed:", err)
return
}
// do something with the initialized resources here
//...
}
在Go语言中,一般资源的分配和释放是通过defer关键字实现的。通过注册资源释放器,在main()函数返回之前,释放掉所有分配的资源。
通道关闭
c := make(chan int)
close(c) // 将通道关闭
_, ok := <-c // 从已经关闭的通道接收元素将阻塞
if!ok { // 判断通道是否已关闭
// 此处可以再次发送或接收元素
} else {
// 此处不能发送或接收元素
}
当通道被关闭之后,再从通道中读取数据,或者再次尝试往通道中发送数据,都会导致程序阻塞,等待超时,直到另一端调用接收方的 close() 函数,或者主动关闭。所以在接收或发送之前,应该先检查一下通道是否已被关闭。
4.具体代码实例和解释说明
使用panic函数
func f() {
if condition {
// call panic to abort the program execution and print an error message
panic("something went wrong!")
}
// rest of the code that might panic
}
当调用者在调用f()函数时,如果condition为true,则调用panic()函数将导致程序的终止,并打印相应的错误信息。如果condition为false,则程序继续正常执行。
捕获panic函数
func main() {
defer func() {
if err := recover(); err!= nil {
fmt.Println("ERROR:", err)
}
}()
// calling functions that may cause panics here
f()
}
func f() {
if true {
// call panic to abort the program execution and print an error message
panic("something went wrong in f()!")
}
// rest of the code inside f() that might panic
}
在调用函数f()时,如果condition为true,则调用panic()函数将导致程序的终止,并打印相应的错误信息。如果condition为false,则程序继续正常执行。
用recover函数来处理panic
func handlePanic(msg string) {
r := recover()
if r!= nil {
fmt.Println("PANIC:", msg, "; RECOVERING FROM:", r)
}
}
func main() {
defer handlePanic("in main()")
// calling functions that may cause panics here
go worker() // create a new goroutine for worker() to run concurrently
time.Sleep(time.Second*10) // sleep for a while before closing the program
fmt.Println("Program ended normally.")
}
func worker() {
defer handlePanic("in worker()")
// doing work here
fmt.Println("Doing some work...")
// simulating some errors by intentionally dividing by zero or accessing invalid pointers
fmt.Println("Number divided by zero is:", 10 / 0)
x := nil
_ = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Sizeof(x)))
}
在调用worker()函数时,如果 intentionally dividing by zero 或 accessing invalid pointers 时,将会导致panic()函数被调用。recover()函数的作用是在panic()函数被调用后,将捕获到错误信息并打印出来。在例子中,每个defered函数都是一段处理错误的代码,这样就不需要编写大量的recover()函数来处理不同的异常类型了。