Inspired By:
最近联调的时候遇到一个
gRPC Server
某个接口返回数据为nil
的情况,因为Server
正常且实现了该接口,所以在调用时没有报错,但是程序突然终止了,也没有任何日志打印。问题很容易复现,直接模拟返回数据为nil
,Debug
模式下也能看到panic
和调用栈信息,果然是也因为对nil
取了内部字段导致的panic
。虽然原因是Server
端处理不正确,但是Client
也要加上防护,能够捕获到panic
。Panic
Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.
常见的
panic
包括空指针引用,数组越界,map
并发等。Recover
Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.
recover
也是Go一个内建函数,用于捕获panic
引发的错误,避免程序直接崩溃退出。
最初的代码结构大致如下:
1func main() { 2 go func() { 3 for { 4 select { 5 case <-c.menuItems.start.ClickedCh: 6 getSthViaGRPC() 7 } 8 }() 9} 10 11func getSthViaGRPC() string { 12 panic("") 13}
一开始想当然的认为既然整个程序退出了,那么直接在主协程里加上
recover
,就可以一劳永逸了,结果发现程序仍然直接崩溃了。改到getSthViaGRPC
函数下可以捕获到,但是每个函数实现都写一个也不现实。查了下是panic
只会触发当前Goroutine
的defer
,也就是说我另起了协程处理,要在对应的协程内捕获。1go func() { 2 defer func() { 3 if r := recover(); r != nil { 4 logger.Errorf("异常: %v\n调用栈:\n %s", r, string(debug.Stack())) 5 } 6 }() 7 for { 8 select { 9 case <-c.menuItems.start.ClickedCh: 10 getSthViaGRPC() 11 } 12}()
好消息是终于捕获到了,坏消息是程序卡死了,看了下代码虽然捕获到了,但是也退出了
for
循环,程序实际没有任何用了,遂封装了一个safeHandler
:1func (c *AppCommand) safeHandler(handler func()) { 2 defer func() { 3 if r := recover(); r != nil { 4 logger.Errorf("异常: %v\n调用栈:\n %s", r, string(debug.Stack())) 5 } 6 }() 7 handler() 8} 9 10go func() { 11 defer func() { 12 if r := recover(); r != nil { 13 logger.Errorf("异常: %v\n调用栈:\n %s", r, string(debug.Stack())) 14 } 15 }() 16 for { 17 select { 18 case <-c.menuItems.start.ClickedCh: 19 safeHandler(getSthViaGRPC) 20 } 21}()
小结:
panic
只会触发当前Goroutine
的defer
,跨协程失效,允许在defer
中嵌套多次调用;recover
只有在defer
中调用才会生效;
💬Discussion