1. 异常设计思想

Go 语言的错误处理思想及设计包含以下特征:

  • 一个可能造成错误的函数,需要返回值中返回一个错误接口( error ),如果调用是成功的,错误接口将返回 nil ,否则返回错误。

  • 在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理。

Go 里没有用经典的 try/except 捕获异常。Go 提供两种错误处理方式

  • 函数返回 error 类型对象判断错误
  • panic 异常

一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine (可以先理解成线程)中被延迟的函数( defer 机制),随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息, panic value 通常是某种错误信息。

虽然 Go 语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于 panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。

任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用 Go 语言提供的错误机制,而不是 panic

一般情况下在 Go 里只使用 error 类型判断错误, Go 官方希望开发者能够很清楚的掌控所有的异常,在每一个可能出现异常的地方都返回或判断 error 是否存在。

panic可以手工调用,但是 Go 官方建议尽量不要使用panic,每一个异常都应该用 error 对象捕获。

如果异常出现了,但没有被捕获并恢复,Go 程序的执行就会被终止,即便出现异常的位置不在主 Goroutine 中也会这样。

2. 如何触发 panic

使用 panic 抛出异常后,函数执行将从调用 panic 的地方停止,如果函数内有 defer 调用,则执行 defer 后边的函数调用,如果 defer 调用的函数中没有捕获异常信息,这个异常会沿着函数调用栈往上传递,直到 main 函数仍然没有捕获异常,将会导致程序异常退出。示例代码:

package main
func demo() {panic("抛出异常")
}
func main() {demo()
}
package mainimport ("fmt"
)func main() {panic("crash")fmt.Println("end")}

输出结果:

panic: crashgoroutine 1 [running]:
main.main()/home/wohu/gocode/src/hello.go:8 +0x39
exit status 2

以上代码中只用了一个内建的函数 panic() 就可以造成崩溃, panic() 的声明如下:

func panic(v interface{})    //panic() 的参数可以是任意类型的。

请谨慎使用panic 函数抛出异常,如果没有捕获异常,将会导致程序异常退出

3. 触发 panic 延迟执行

Go 中,panic 主要有两类来源,一类是来自 Go 运行时,另一类则是 Go 开发人员通过 panic 函数主动触发的。

panic() 触发的宕机发生时, panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用,参考下面代码:

package mainimport ("fmt"
)func main() {defer fmt.Println("defef run")panic("crash")fmt.Println("end")}

输出结果:

defef run
panic: crashgoroutine 1 [running]:
main.main()/home/wohu/gocode/src/hello.go:10 +0x95
exit status 2

从结果中可以看到,触发 panic 前, defer 语句会被优先执行。

panic() 是一个内建函数,可以中断原有的控制流程,进入一个令人 panic 的流程中。当函数 main 调用 panic,函数的执行被中断,但是 main 中的延迟函数(必须是在 panic 之前的已加载的 defer )会正常执行,然后 main 返回到调用它的地方。在调用的地方,main 的行为就像调用了 panic。这一过程继续向上,直到发生 panicgoroutine 中所有调用的函数返回,此时程序退出。

异常可以直接调用 panic 产生。也可以由运行时错误产生,例如访问越界的数组。

4. recover 使用

recover 是一个 Go 语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来。

recover 仅在延迟函数 defer 中有效

  • 在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果;

  • 如果当前的 goroutine 陷入 panic ,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行;

注意:

在其他语言里, panic 往往以异常的形式存在,底层抛出异常,上层逻辑通过 try/catch 机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。

Go 语言没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常, recover 的宕机恢复机制就对应其它语言中的 try/catch 机制。

package mainfunc test() {defer func() {if err := recover(); err != nil { // recover 捕获错误。println(err.(string)) // 将 interface{} 转型为具体类型。}}()panic("panic error!")    // panic 抛出错误
}
func main() {test()
}

由于 panicrecover 参数类型为 interface{} ,因此可抛出任何类型对象。

func panic(v interface{})
func recover() interface{}

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

package mainimport "fmt"func test() {defer func() {fmt.Println(recover())}()defer func() {panic("defer panic")}()panic("test panic")
}
func main() {test()
}

输出:

defer panic

捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil 。任何未捕获的错误都会沿调用堆栈向外传递。

当没有异常信息抛出时, recover 函数返回值是 nilrecover 只有在 defer 调用的函数内部时,才能阻止 panic 抛出的异常信息继续向上传递,如果不是在 defer 调用的函数内部,将会失效。

package mainimport "fmt"func test() {defer recover()              // 无效!defer fmt.Println(recover()) // 无效!defer func() {func() {println("defer inner")recover() // 无效!}()}()panic("test panic")
}
func main() {test()
}

输出

defer inner
<nil>
panic: test panic

使用延迟匿名函数或下面这样都是有效的。

package mainimport "fmt"func except() {fmt.Println(recover())
}
func test() {defer except()panic("test panic")
}
func main() {test()
}

如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。

package mainimport "fmt"func test(x, y int) {var z intfunc() {defer func() {err := recover()fmt.Println(err)if err != nil {z = 0}}()z = x / yreturn}()println("x / y =", z)
}
func main() {test(10, 0)
}

输出结果:

runtime error: integer divide by zero
x / y = 0

recover 的正确用法:

package mainimport ("errors""fmt"
)func main() {fmt.Println("Enter function main.")defer func() {fmt.Println("Enter defer function.")// recover函数的正确用法。if p := recover(); p != nil {fmt.Printf("panic: %s\n", p)}fmt.Println("Exit defer function.")}()// recover函数的错误用法。fmt.Printf("no panic: %v\n", recover())// 引发panic。panic(errors.New("something wrong"))// recover函数的错误用法。p := recover()fmt.Printf("panic: %s\n", p)fmt.Println("Exit function main.")
}

5. panicrecover 的关系

如何区别使用 panicerror 两种方式?

惯例是:导致关键流程出现不可修复性错误的使用 panic ,其他使用 error

panicrecover 的组合有如下特性:

  • panicrecover ,程序宕机。
  • panic 也有 recover ,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。

注意:

虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。

panic 触发的 defer 函数内,可以继续调用 panic ,进一步将错误外抛,直到程序整体崩溃。

如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置

6. 实际项目使用

Go 并发编程中,每一个 goroutine 出现 panic,都会让整个进程退出,如果能够捕获异常,那么出现 panic 的时候,整个服务不会挂掉,只是当前导致 panic 的某个 goroutine 会出现异常,通过捕获异常可以继续执行任务,建议还是在某些有必要的条件和入口处进行异常捕获。

常见抛出异常的情况:数组越界、空指针空对象,类型断言失败等。

package mainimport ("fmt""time"
)// 抛出异常,模拟实际 Panic 的场景
func throwException() {panic("An exception is thrown! Start Panic")
}// Go 的 defer + recover 来捕获异常
func catchExceptions() {defer func() {if e := recover(); e != nil {fmt.Printf("Panicing %s\n", e)}}()go func() {// 做具体的实现任务fmt.Print("do something \n")}()throwException()fmt.Printf("Catched an exceptions\n")
}func main() {fmt.Printf("==== start main =====\n")// 执行一次catchExceptions()num := 1for {num++fmt.Printf("\nstart circle num:%v\n", num)// 循环执行,如果实际项目中,这个函数是主任务的话,需要一个 for 来循环执行,避免捕获一次 Panic 之后就不再继续执行catchExceptions()time.Sleep(3 * time.Second)if num == 5 {fmt.Printf("==== end main =====\r\n")return}}
}

一般的建议是在请求来源入口处的函数或者关键路径上实现这么一段代码进行捕获,这样,只要通过这个入口出现的异常都能被捕获,并打印详细日志。同时,为了保证 goroutine 能够继续执行任务,因此还要考虑当出现 panic 被捕获之后,是否有主动循环或者被动触发来重新执行任务。

7. 如何应对 panic

7.1 评估程序对 panic 的忍受度

Go 标准库提供的 http server 采用的是,每个客户端连接都使用一个单独的 Goroutine 进行处理的并发处理模型。也就是说,客户端一旦与 http server 连接成功,http server 就会为这个连接新创建一个 Goroutine,并在这 Goroutine 中执行对应连接(conn)的 serve 方法,来处理这条连接上的客户端请求。

无论在哪个 Goroutine 中发生未被恢复的 panic,整个程序都将崩溃退出。所以,为了保证处理某一个客户端连接的 Goroutine 出现 panic 时,不影响到 http serverGoroutine 的运行,Go 标准库在 serve 方法中加入了对 panic 的捕捉与恢复,下面是 serve 方法的部分代码片段:

// $GOROOT/src/net/http/server.go
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {c.remoteAddr = c.rwc.RemoteAddr().String()ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())defer func() {if err := recover(); err != nil && err != ErrAbortHandler {const size = 64 << 10buf := make([]byte, size)buf = buf[:runtime.Stack(buf, false)]c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)}if !c.hijacked() {c.close()c.setState(c.rwc, StateClosed, runHooks)}}()... ...
}

你可以看到,serve 方法在一开始处就设置了 defer 函数,并在该函数中捕捉并恢复了可能出现的 panic。这样,即便处理某个客户端连接的 Goroutine 出现 panic,处理其他连接 Goroutine 以及 http server 自身都不会受到影响。

这种局部不要影响整体的异常处理策略,在很多并发程序中都有应用。并且,捕捉和恢复 panic 的位置通常都在子 Goroutine 的起始处,这样设置可以捕捉到后面代码中可能出现的所有 panic,就像 serve 方法中那样。

7.2 提示潜在 bug

json 包的 encode.go 中也有使用 panic 做潜在 bug 提示的例子:

// $GOROOT/src/encoding/json/encode.go
func (w *reflectWithString) resolve() error {... ...switch w.k.Kind() {case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:w.ks = strconv.FormatInt(w.k.Int(), 10)return nilcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:w.ks = strconv.FormatUint(w.k.Uint(), 10)return nil}panic("unexpected map key type")
}

这段代码中,resolve 方法的最后一行代码就相当于一个“代码逻辑不会走到这里”的断言。一旦触发“断言”,这很可能就是一个潜在 bug。

我们也看到,去掉这行代码并不会对 resolve 方法的逻辑造成任何影响,但真正出现问题时,开发人员就缺少了“断言”潜在 bug 提醒的辅助支持了。在 Go 标准库中,大多数 panic 的使用都是充当类似断言的作用的。

Go 学习笔记(19)— 函数(05)[如何触发 panic、触发 panic 延迟执行、panic 和 recover 的关系]相关推荐

  1. Python学习笔记19:列表 III

    Python学习笔记19:列表 III 其实这篇笔记标题应该是列表扩展,从列表开始,将涵盖Python中的序列容器. 关于列表的基础知识,可以看我的前两篇文章: Python学习笔记1:列表. Pyt ...

  2. Linux 学习笔记19 信号

    Linux 学习笔记19 信号 信号 信号概述 为什么要是使用信号--为了实现进程的有序退出 信号是进程运行过程中,由自身产生或者由进程外部发来的消息.信号是硬件中断的软件模拟(软中断) signal ...

  3. python函数是一段具有特定功能的语句组_Python学习笔记(五)函数和代码复用

    本文将为您描述Python学习笔记(五)函数和代码复用,具体完成步骤: 函数能提高应用的模块性,和代码的重复利用率.在很多高级语言中,都可以使用函数实现多种功能.在之前的学习中,相信你已经知道Pyth ...

  4. Ext.Net学习笔记19:Ext.Net FormPanel 简单用法

    Ext.Net学习笔记19:Ext.Net FormPanel 简单用法 FormPanel是一个常用的控件,Ext.Net中的FormPanel控件同样具有非常丰富的功能,在接下来的笔记中我们将一起 ...

  5. springmvc学习笔记(19)-RESTful支持

    springmvc学习笔记(19)-RESTful支持 标签: springmvc springmvc学习笔记19-RESTful支持 概念 REST的样例 controller REST方法的前端控 ...

  6. Python学习笔记:函数(Function)

    Python学习笔记:函数(Function) 一.函数基本概念 函数是Python里组织与重用代码最重要的方法.一般来说,如果你期望多次重复相同或相似的代码,写一个可重用的函数可能是值得的.函数通过 ...

  7. php中声明一个函数,php学习笔记之 函数声明

    /* 函数定义: * 1.函数是一个被命名的 * 2.独立的代码段 * 3.函数执行特定任务 * 4.并可以给调用它的程序返回一个值 * * 函数的优点: * 1.提高程序的重用性 * 2.提高程序的 ...

  8. Matlab学习笔记 figure函数

    Matlab学习笔记 figure函数 matlab中的 figure 命令,能够创建一个用来显示图形输出的一个窗口对象.每一个这样的窗口都有一些属性,例如窗口的尺寸.位置,等等.下面一一介绍它们. ...

  9. 区块链学习笔记19——ETH难度调整

    区块链学习笔记19--ETH难度调整 学习视频:北京大学肖臻老师<区块链技术与应用> 笔记参考:北京大学肖臻老师<区块链技术与应用>公开课系列笔记--目录导航页 前面学过,比特 ...

  10. JAVA学习笔记五---函数

    JAVA学习笔记五---函数 5.1 方法的学习 编写一个程序,求圆的周长和面积. package practice; /*** 编写一个程序,求圆的周长和面积.* @author iszhangyo ...

最新文章

  1. MySQL的4中隔离级别
  2. VMware虚拟机中CentOS根分区的扩展
  3. 数组 ——求出一组数的最大值(用数组实现)
  4. fastbin attack攻击中关于 malloc__hook
  5. angular_ui-router ——依赖注入
  6. 【Linux网络编程】 网络协议入门
  7. [SCOI2009]最长距离
  8. monterey系统怎么降级?macOS Monterey系统降回Big Sur的详细教程
  9. ELK下Kibana和Elasticsearch之间相互TLS身份验证
  10. nyoj 309 bobsledding 即河南省第四届大学生程序设计大赛第七题
  11. 基于matlab单目视觉焊缝跟踪系统,基于激光线结构光3D视觉的机器人轨迹跟踪方法与应用...
  12. 全网首发:linux任务栏分组的研究
  13. 中文简体繁体转换(JS 字符串 简体转繁体 繁体转简体)
  14. [Daozy][区块链 EOS 课程]第2课 EOS编译和启动
  15. 门函数卷积_卷积及其应用
  16. 说说域名、二级域名和主机名的联系区别
  17. Arduino驱动HDC1080测量温湿度
  18. Vue中时间日期格式化
  19. java基础知识面试题(41-95)
  20. ubuntu IPV6及作为路由分配【笔记】

热门文章

  1. 自制青蛙跳台阶小游戏~
  2. 2021-2027年中国医疗美容市场研究及前瞻分析报告
  3. 并发 vs 并行 (Concurrency Is Not Parallelism)
  4. 判断某数组是不是二叉树的前序遍历序列 python递归
  5. 使用 Pytorch 实现 skip-gram 的 word2vec
  6. LeetCode简单题之判断矩阵经轮转后是否一致
  7. SoC(System on chip)与NoC(network-on-chip)
  8. ALD技术,相机去噪,图像传感器
  9. 2021年大数据Flink(三十九):​​​​​​​Table与SQL ​​​​​​总结 Flink-SQL常用算子
  10. getCacheDir() 和 getFilesDir() 的区别