Panicking like a Gopher

Michał Łowicki
golangspec

--

Errors while executing program in Go (after successful compilation and when OS process has been started) take the form of panics. They can be triggered in two ways:

  1. consciously using built-in panic function:

panic takes as an argument any value of type implementing empty interface (interface{}) which is satisfied by all types.

2. caused by programming error, producing run-time panic:

Triggering run-time panics is semantically the same as calling panic function with values of interface type runtime.Error.

2nd output has additional information about signal. 0x8 (SIGFPE) reports fatal arithmetic error.

To fully understand mechanics of panicking let’s first deep dive into the structure of running Go program.

Goroutines

Program implemented in Go during its execution consists of one or more goroutines. Specification defines goroutine in the section about go statement:

A “go” statement starts the execution of a function call as an independent concurrent thread of control, or goroutine, within the same address space.

Go is a concurrent language. This is because it has been shipped with features supporting concurrent programming like statement to run things concurrently (go statement) or mechanism to easily communicate between such concurrent things (channels). What it means to be concurrent though? How it’s related to omnipresent parallelism?

Composition

Concurrency is a way of building things as a set of independent tasks. It’s about a structure (design). Concurrent program handles separate tasks no matter f.ex. what is the order of their execution. Parallelism is a simultaneous execution of two or more tasks. Literally at the same time many threads of execution are making progress. It needs to be done on multi-core machine — there is not way to “mimic” it.

Concurrency is a more general term than parallelism. Runtime of concurrent program can (but doesn’t have to) take advantage of multiple cores and run many computations simultaneously. If only one core is available then using f.ex. time slicing (divide time into discrete intervals and assign them to different tasks) it’s still concurrent but parallelism is not technically possible.

Concurrency is about dealing with a lot of things at once. Parallelism is about doing a lot of things at once.

Rob Pike

The main goroutine

It runs main function from main package (an entry point for every program written in Go). If this goroutine ends it execution, the whole program terminates. The runtime doesn’t wait for other goroutines to complete then.

Program starts with single (main) goroutine and during its life-cycle can create new ones (isn’t unusual to have millions of them).

Goroutines live in the same address space

Defer statement

It enables to execute function(s) after the encompassing function (the one where defer is used) ends which happens:

  1. on return statement:

2. while reaching end of function’s body:

3. during panicking:

Function value and passed arguments are evaluated at the point of defer statement not when actual call takes place:

Each function can have many defer statements inside. The order of calls is last-in first-out (like deferred calls would be put onto the stack):

Methods calls also valid:

and it outputs “Inside method” as you might expect.

When function value evaluates to nil, program will panic. It won’t happened at the point where defer statement is evaluated though but when actual calling of deferred functions kicks in:

It’s possible inside deferred function to tamper named return parameter. If it isn’t named then changing value of returned variable through closure doesn’t take any effect:

As we’ll see below defer statements are heavily used to handle panics (of course they can be used for different things as well, not necessarily for dealing with errors).

Panicking

When arbitrary function f panics, we’ve seen from examples higher up that function calls deferred in f will be called in last-in first-out order. What happens then? Afterwards for f’s caller such process will be repeated — its deferred functions will triggered. And so on until the top-level function in f’s goroutine. Finally deferred functions for top-level function are called and program terminates. It’s like a bubbling up to the top of the calls chain:

It’s worth to note that no matter in which goroutine it all starts (main or other crated later on), the whole program crashes.

Panicking even more

What will happen after triggering new panic inside deferred function?

It turns out that the whole dance with calling deferred functions up to the top of calls chain will be executed anyway. What is new though is that second panic will be also displayed like in the output above.

Recover

Built-in recover function allows to see if panic has been triggered and stops its propagation. Returned value is either parameter passed to panic (if panicking is in progress) or nil. After call, the current panicking sequence stops and the program behaves like the panic never happened from the point of caller’s of function inside which deferred function calls recover:

It’s allowed to call recover outside deferred function but nil will be always returned.

Return value of call to recover in deferred function when there is no active panic is nil. If panic is called with nil as parameter there is no way to distinguish whether panic was in progress or not.

If you like the post and want to get updates about new ones please follow me. Help others discover this story by clicking ❤ below.

Resources

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.