1. Background
Recently, during a joint debugging session, I encountered a situation where a gRPC Server
returned nil
for a specific interface. Since the Server
was functioning normally and the interface was implemented, there were no errors during the call. However, the program suddenly terminated without any log output. The issue was easily reproducible by simulating the return of nil
data. In Debug
mode, I could see the panic
and the call stack information. It turned out that the panic
was caused by accessing an internal field of a nil
value. Although the root cause was incorrect handling on the Server
side, it was also necessary for the Client
to implement safeguards to catch the panic
.
2. Concepts
1. 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.
Common causes of panic
include nil pointer dereference, array out-of-bounds access, and concurrent map writes.
2. 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
is another built-in function in Go that is used to catch panic
-induced errors, preventing the program from crashing outright.
3. Implementation
The initial code structure was roughly as follows:
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}
At first, I naively thought that adding recover
in the main goroutine would solve the problem once and for all. However, the program still crashed. Moving recover
to the getSthViaGRPC
function worked, but it was impractical to implement this in every function. After some research, I found that panic
only triggers defer
in the current goroutine. Since I had started a new goroutine, I needed to catch the panic
within that goroutine.
1go func() {
2 defer func() {
3 if r := recover(); r != nil {
4 logger.Errorf("Error: %v\nStack trace:\n %s", r, string(debug.Stack()))
5 }
6 }()
7 for {
8 select {
9 case <-c.menuItems.start.ClickedCh:
10 getSthViaGRPC()
11 }
12}()
The good news was that the panic
was finally caught. The bad news was that the program froze. Upon reviewing the code, I realized that although the panic
was caught, the for
loop had exited, rendering the program useless. I then encapsulated the logic into a safeHandler
:
1func (c *AppCommand) safeHandler(handler func()) {
2 defer func() {
3 if r := recover(); r != nil {
4 logger.Errorf("Error: %v\nStack trace:\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("Error: %v\nStack trace:\n %s", r, string(debug.Stack()))
14 }
15 }()
16 for {
17 select {
18 case <-c.menuItems.start.ClickedCh:
19 safeHandler(getSthViaGRPC)
20 }
21}()
4. Summary
Panic
only triggersdefer
in the current goroutine. It does not work across goroutines, but it allows for nested calls withindefer
.Recover
only works when called within adefer
function.
5. Common Panics
panic: assignment to entry in nil map
Attempting to assign a value to an uninitialized
map
. In Go,map
is a reference type and must be initialized usingmake
or a literal before use. Otherwise, its value isnil
, and trying to insert data into anil
map
will cause a runtimepanic
.