현재 3권의 신간들인 Go Optimizations 101, Go Details & Tips 101Go Generics 101이 출간되어 있습니다. Leanpub 서점에서 번들을 모두 구입하시는 방법이 비용 대비 효율이 가장 좋습니다.

Go에 대한 많은 정보들과 Go 101 책들의 최신 소식을 얻으시려면 Go 101 트위터 계정인 @Go100and1을 팔로잉 해주세요.

Explain Panic/Recover Mechanism in Detail

Panic and recover mechanism has been introduced before, and several panic/recover use cases are shown in the last article. This current article will explain panic/recover mechanism in detail. Exiting phases of function calls will also be explained in detail.

Exiting Phases of Function Calls

In Go, a function call may undergo an exiting phase before it fully exits. In the exiting phase, the deferred function calls pushed into the deferred call queue during executing the function call will be executed (in the inverse pushing order). When all of the deferred calls fully exit, the exiting phase ends and the function call also fully exits.

Exiting phases might also be called returning phases elsewhere.

A function call may enter its exiting phase (or exit directly) through three ways:
  1. after the call returns normally.
  2. when a panic occurs in the call.
  3. after the runtime.Goexit function is called and fully exits in the call.
For example, in the following code snippet,
import (
	"fmt"
	"runtime"
)

func f0() int {
	var x = 1
	defer fmt.Println("exits normally:", x)
	x++
	return x
}

func f1() {
	var x = 1
	defer fmt.Println("exits normally:", x)
	x++
}

func f2() {
	var x, y = 1, 0
	defer fmt.Println("exits for panicking:", x)
	x = x / y // will panic
	x++       // unreachable
}

func f3() int {
	x := 1
	defer fmt.Println("exits for Goexiting:", x)
	x++
	runtime.Goexit()
	return x+x // unreachable
}

BTW, the runtime.Goexit() function is not intended to be called in the main goroutine of a program.

Associating Panics and Goexit Signals of Function Calls

When a panic occurs directly in a function call, we say the (unrecovered) panic starts associating with the function call. Similarly, when the runtime.Goexit function is called in a function call, we say a Goexit signal starts associating with the function call after the runtime.Goexit call fully exits. As explained in the last section, associating either a panic or a Goexit signal with a function call will make the function call enter its exiting phase immediately.

We have learned that panics can be recovered. However, there are no ways to cancel a Goexit signal.

At any give time, a function call may associate with at most one unrecovered panic. If a call is associating with an unrecovered panic, then For example, in the following program, the recovered panic is panic 3, which is the last panic associating with the main function call.
package main

import "fmt"

func main() {
	defer func() {
		fmt.Println(recover()) // 3
	}()
	
	defer panic(3) // will replace panic 2
	defer panic(2) // will replace panic 1
	defer panic(1) // will replace panic 0
	panic(0)
}

As Goexit signals can't be cancelled, arguing whether a function call may associate with at most one or more than one Goexit signal is unnecessary.

Although it is unusual, there might be multiple unrecovered panics coexisting in a goroutine at a time. Each one associates with one non-exited function call in the call stack of the goroutine. When a nested call fully exits and it still associates with an unrecovered panic, the unrecovered panic will spread to the nesting call (the caller of the nested call). The effect is the same as a panic occurs directly in the nesting call. That says,

So, when a goroutine finishes to exit, there may be at most one unrecovered panic in the goroutine. If a goroutine exits with an unrecovered panic, the whole program crashes, and the information of the unrecovered panic will be reported. Otherwise, the goroutine exits normally (peacefully).

When a function is invoked, there is neither a panic nor Goexit signals associating with its call initially, no matter whether its caller (the nesting call) has entered exiting phase or not. Surely, panics might occur or the runtime.Goexit function might be called later in the process of executing the call, so panics and Goexit signals might associate with the call later.

The following example program will crash if it runs, because the panic 2 is still not recovered when the new goroutine exits.
package main

func main() {
	// The new goroutine.
	go func() {
		// This is an anonymous deferred call.
		// When it fully exits, the panic 2 will spread
		// to the entry function call of the new
		// goroutine, and replace the panic 0. The
		// panic 2 will never be recovered.
		defer func() {
			// As explained in the last example,
			// panic 2 will replace panic 1.
			defer panic(2)
			
			// When the anonymous function call fully
			// exits, panic 1 will spread to (and
			// associate with) the nesting anonymous
			// deferred call.
			func () {
				// Once the panic 1 occurs, there will
				// be two unrecovered panics coexisting
				// in the new goroutine. One (panic 0)
				// associates with the entry function
				// call of the new goroutine, the other
				// (panic 1) associates with the
				// current anonymous function call.
				panic(1)
			}()
		}()
		panic(0)
	}()
	
	select{}
}
The output (when the above program is compiled with the standard Go compiler v1.20):
panic: 0
	panic: 1
	panic: 2

...

The format of the output is not perfect, it is prone to make some people think that the panic 0 is the final unrecovered panic, whereas the final unrecovered panic is actually panic 2.

Similarly, when a nested call fully exits and it is associating with a Goexit signal, then the Goexit signal will also spread to (and associate with) the nesting call. This will make the nesting call enter (if it hasn't entered) its exiting phase immediately.

When a Goexit signal associates with a function call, if the function call is associating with an unrecovered panic, then the panic will be recovered. For example, the following program will exit peacefully and print <nil>, because the bye panic will be recovered by the Goexit signal.
package main

import (
	"fmt"
	"runtime"
)

func f() {
	defer func() {
		fmt.Println(recover())
	}()

	// The Goexit signal will disable the "bye" panic.
	defer runtime.Goexit()
	panic("bye")
}

func main() {
	go f()
	
	for runtime.NumGoroutine() > 1 {
		runtime.Gosched()
	}
}

Some recover Calls Are No-Ops

The builtin recover function must be called at proper places to take effect. Otherwise, the calls are no-ops. For example, none of the recover calls in the following example recover the bye panic.
package main

func main() {
	defer func() {
		defer func() {
			recover() // no-op
		}()
	}()
	defer func() {
		func() {
			recover() // no-op
		}()
	}()
	func() {
		defer func() {
			recover() // no-op
		}()
	}()
	func() {
		defer recover() // no-op
	}()
	func() {
		recover() // no-op
	}()
	recover()       // no-op
	defer recover() // no-op
	panic("bye")
}

We have already known that the following recover call takes effect.
package main

func main() {
	defer func() {
		recover() // take effect
	}()

	panic("bye")
}

Then why don't those recover calls in the first example of the current section take effect? Let's read the current version of Go specification:
The return value of recover is nil if any of the following conditions holds:
  • panic's argument was nil;
  • the goroutine is not panicking;
  • recover was not called directly by a deferred function.

There is an example showing the first condition case in the last article.

Most of the recover calls in the first example of the current section satisfy either the second or the third conditions mentioned in Go specification, except the first call. Yes, here, the current descriptions are not precise yet. The thrid condition should be described as

In the first example of the current section, the expected to-be-recovered panic is associating with the main function call. The first recover call is called directly by a deferred function call but the deferred function call is not called directly by the main function call. This is why the first recover call is a no-op.

In fact, the current Go specification also doesn't explain well why the second recover call (by code line order), which is expected to recover panic 1, in the following example doesn't take effect.
// This program exits without recovering panic 1.
package main

func demo() {
	defer func() {
		defer func() {
			recover() // this one recovers panic 2
		}()

		defer recover() // no-op

		panic(2)
	}()
	panic(1)
}

func main() {
	demo()
}

What Go specification doesn't mention is that, each recover call is viewed as an attempt to recover the newest unrecovered panic in the current goroutine.

Go runtime thinks the second recover call in the above example attempts to recover the newest unrecovered panic, panic 2, which is associating with the caller call of the second recover call. The second recover call is not called directly by a deferred function call which is called by the associating function call. Instead, it is directly called by the associating function call. This is why the second recover call is a no-op.

Summary

OK, now, let's try to make a short description on which recover calls will take effect:
A recover call takes effect only if the direct caller of the recover call is a deferred call and the direct caller of the deferred call associates with the newest unrecovered panic in the current goroutine. An effective recover call disassociates the newest unrecovered panic from its associating function call, and returns the value passed to the panic call which produced the newest unrecovered panic.

Index↡

The Go 101 프로젝트는 Github 에서 호스팅됩니다. 오타, 문법 오류, 부정확한 표현, 설명 결함, 코드 버그, 끊어진 링크와 같은 모든 종류의 실수에 대한 수정 사항을 제출하여 Go 101을 개선을 돕는 것은 언제나 환영합니다.

주기적으로 Go에 대한 깊이 있는 정보를 얻고 싶다면 Go 101의 공식 트위터 계정인 @go100and1을 팔로우하거나 Go 101 슬랙 채널에j가입해주세요.

이 책의 디지털 버전은 아래와 같은 곳을 통해서 구매할 수 있습니다.
Go 101의 저자인 Tapir는 2016년 7월부터 Go 101 시리즈 책들을 집필하고 go101.org 웹사이트를 유지 관리하고 있습니다. 새로운 콘텐츠는 책과 웹사이트에 수시로 추가될 예정입니다. Tapir는 인디 게임 개발자이기도 합니다. Tapir의 게임을 플레이하여 Go 101을 지원할 수도 있습니다. (안드로이드와 아이폰/아이패드용):
  • Color Infection (★★★★★), 140개 이상의 단계로 이루어진 물리 기반의 캐주얼 퍼즐 게임
  • Rectangle Pushers (★★★★★), 2가지 모드와 104개 이상의 단계로 이루어진 캐주얼 퍼즐 게임
  • Let's Play With Particles, 세가지 미니 게임이 있는 캐주얼 액션 게임
페이팔을 통한 개인 기부도 환영합니다.

색인: