Utilization of Defer in Go

Utilization of Defer in Go

Defer and clear code.

Defer in Go is the command of deferred execution of an action before completing the main function. Defer is similar to spring that closes an open door in severe winter. 

A popular example is closing a file or closing a database connection. 

func FileOperationsExample() error {
	f, err := os.Create("/tmp/defer.txt")
	if err != nil {
		return err
	}
	defer f.Close()

	// write to file or any other operation

	return nil
}

One more example of locking and unlocking:

import "sync"

type CurrencyRateService struct {
	data map[string]map[string]float64
	m    sync.RWMutex
}

func (s *CurrencyRateService) Update(data map[string]map[string]float64) {
	s.m.Lock()
	defer s.m.Unlock()

	s.data = data
}

func (s *CurrencyRateService) Get(fromCurrencyCode, toCurrencyCode string) float64 {
	s.m.RLock()
	defer s.m.RUnlock()

	return s.data[fromCurrencyCode][toCurrencyCode]
}

This is relevant in the examples, which are more complex than CurrencyRateService, where is better: 

func (s *CurrencyRateService) Update(data map[string]map[string]float64) {
	s.m.Lock()
	s.data = data
	s.m.Unlock()
}

func (s *CurrencyRateService) Get(fromCurrencyCode, toCurrencyCode string) float64 {
	s.m.RLock()
	rate := s.data[fromCurrencyCode][toCurrencyCode]
	s.m.RUnlock()

	return rate
}

Defer in Go and access the results of the function.

For example, let’s take a simple function, that has a named return values:

func ReturnOne() (result int) {
	result = 1

	return
}

Using defer to change the results:

func RewriteReturnOne() (result int) {
	defer func() {
		result = 2
	}()

	result = 1

	return
}

func TestRewriteReturnOne(t *testing.T) {
	assert.Equal(t, 2, RewriteReturnOne())
}
func RewriteReturnOneWithoutAssign() (result int) {
	defer func() {
		result = 3
	}()

	return 1
}

func TestRewriteReturnOneWithoutAssign(t *testing.T) {
	assert.Equal(t, 3, RewriteReturnOneWithoutAssign())
}

These examples are clear, also defer has the access to the value, which were established before return:

func ModifyReturnOneWithoutAssign() (result int) {
	defer func() {
		result = result * 5
	}()

	return 2
}

func TestModifyReturnOneWithoutAssign(t *testing.T) {
	assert.Equal(t, 10, ModifyReturnOneWithoutAssign())
}

The order of execution of defer in the function

Usually in the examples, the fmt.Println is used for easy launch in the playground in order to show the order of the execution. 

It will be easier to show the order of the execution using tests. Here is an example:

func OneDeferOrder() (result []string) {
	result = append(result, "first")

	defer func() {
		result = append(result, "first defer")
	}()

	result = append(result, "second")

	return result
}

And the test will show the expected results:

func TestOneDeferOrder(t *testing.T) {
	var actual = OneDeferOrder()

	assert.Equal(
		t,
		actual,
		[]string{
			"first",
			"second",

			"first defer",
		},
	)
}

Let’s add one more defer:

func DoubleDeferOrder() (result []string) {
	result = append(result, "first")
	defer func() {
		result = append(result, "first defer")
	}()

	result = append(result, "second")
	defer func() {
		result = append(result, "second defer")
	}()

	result = append(result, "third")

	return result
}

The test that shows that the defer execution order is opposite to the adding order to the execution list:

func TestDoubleDeferOrder(t *testing.T) {
	var order = DoubleDeferOrder()

	assert.Equal(
		t,
		order,
		[]string{
			"first",
			"second",
			"third",

			"second defer",
			"first defer",
		},
	)
}

It’s like unwinding a ball of resources that depend on previous ones, or LIFO.

Defer in Go and call methods chain

Let’s prepare a structure to save the state:

type State struct {
	values []string
}

func (s *State) Append(value string) *State {
	s.values = append(s.values, value)

	return s
}

func (s *State) Values() []string {
	return s.values
}

And the function that will use the call chain:

func OnlyLastHandleDefer(state *State) {
	state.Append("first")

	defer state.
		Append("first defer — first call").
		Append("first defer — second call").
		Append("first defer — last call")

	state.Append("second")
}

The test will show that only the last call will be postponed:

func TestOnlyLastHandleDefer(t *testing.T) {
	var state = new(State)

	OnlyLastHandleDefer(state)

	assert.Equal(
		t,
		state.Values(),
		[]string{
			"first",
			"first defer — first call",
			"first defer — second call",
			"second",
			"first defer — last call",
		},
	)
}

Wrap into a function, we make deferred for all calls in the chain:

func OnlyLastHandleDeferWrap(state *State) {
	state.Append("first")

	defer func() {
		state.
			Append("first defer — first call").
			Append("first defer — second call").
			Append("first defer — last call")
	}()

	state.Append("second")
}

func TestOnlyLastHandleDeferWrap(t *testing.T) {
	var state = new(State)

	OnlyLastHandleDeferWrap(state)

	assert.Equal(
		t,
		state.Values(),
		[]string{
			"first",
			"second",
			"first defer — first call",
			"first defer — second call",
			"first defer — last call",
		},
	)
}

Defer in Go and arguments calculation

Let’s prepare the counter:

import (
	"strconv"
)

type StringCounter struct {
	value uint64
}

func (c *StringCounter) Next() string {
	c.value += 1

	var next = c.value

	return strconv.FormatUint(next, 10)
}

We will write the test and the function to show that arguments will be calculated immediately:

func CallInside(state *State) {
	var counter = new(StringCounter)

	state.Append("first call " + counter.Next())

	defer state.Append("first defer call " + counter.Next())

	state.Append("second call " + counter.Next())
}

func TestCallInside(t *testing.T) {
	var state = new(State)

	CallInside(state)

	assert.Equal(
		t,
		[]string{
			"first call 1",
			"second call 3",
			"first defer call 2",
		},
		state.Values(),
	)
}

The counter.Next() action was executed immediately, so that «first defer call 2». 

If we wrap it to the function, we will get the expected result:

func CallInsideWrap(state *State) {
	var counter = new(StringCounter)

	state.Append("first call " + counter.Next())

	defer func() {
		state.Append("first defer call " + counter.Next())
	}()

	state.Append("second call " + counter.Next())
}

func TestCallInsideWrap(t *testing.T) {
	var state = new(State)

	CallInsideWrap(state)

	assert.Equal(
		t,
		[]string{
			"first call 1",
			"second call 2",
			"first defer call 3",
		},
		state.Values(),
	)
}

Error and panic return

Various expected errors may occur during program execution, in which case in Golang the error is returned as a function.

Standard Golang libraries are full of examples of error recovery, creating a file or writing to a file, or the simplest example:

package strconv

func ParseBool(str string) (bool, error) {
	switch str {
	case "1", "t", "T", "true", "TRUE", "True":
		return true, nil
	case "0", "f", "F", "false", "FALSE", "False":
		return false, nil
	}
	return false, syntaxError("ParseBool", str)
}

When it is not possible to return an error, and there is an error – then there is a panic that can end the program.

As an example, an attempt to call a method before initialization:

import (
	"database/sql"
	"github.com/stretchr/testify/assert"
	"testing"
)

func PanicNilPointer(connection *sql.DB) {
	_ = connection.Ping()
}

func TestPanicNilPointer(t *testing.T) {
	var connection *sql.DB

	PanicNilPointer(connection)
}
panic: runtime error: invalid memory address or nil pointer dereference

Also, panic can be called in the code with the help of command panic.

As an example, in the standard bytes package and the Repeat function causes panic while the arguments are being checked for correctness.

In addition to this, there are functions that start with the world Must and convert error return to panic:

package regexp

func MustCompile(str string) *Regexp {
	regexp, err := Compile(str)
	if err != nil {
		panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
	}
	return regexp
}

Recover, or recover after panic

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. Source

Recover is the built-in function to restore the current goroutine during panic, which is useful only in conjunction with defer.

Recover returns the value, that was transmitted during a panic or nil call.

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestRecover(t *testing.T) {
	var expect interface{}

	var actual = recover()

	assert.Equal(t, true, expect == actual)
}

Here is an example, that will show that during the panic in the current goroutine all the defers will be executed, and the first defer in the execution list that has recovery and will take the value passed to panic. 

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func WrapRecovery(state *State) {
	state.Append("first")
	defer func() {
		if err := recover(); err != nil {
			if errorMessage, ok := err.(string); ok {
				state.Append("first defer — recover string panic: " + errorMessage)
			} else {
				state.Append("first defer — recover panic")
			}
		} else {
			state.Append("first defer — without panic")
		}
	}()

	state.Append("second")
	defer func() {
		if err := recover(); err != nil {
			if errorMessage, ok := err.(string); ok {
				state.Append("second defer — recover string panic: " + errorMessage)
			} else {
				state.Append("second defer — recover panic")
			}
		} else {
			state.Append("second defer — without panic")
		}
	}()

	state.Append("third")
	defer func() {
		state.Append("third defer — without recover")
	}()

	panic("catch me")
}

func TestWrapRecovery(t *testing.T) {
	var state = new(State)

	WrapRecovery(state)

	assert.Equal(
		t,
		[]string{
			"first",
			"second",
			"third",
			"third defer — without recover",
			"second defer — recover string panic: catch me",
			"first defer — without panic",
		},
		state.Values(),
	)
}

For your information, the value of nil can be transmitted into a panic, but the best way is to transmit the string or the error. 

package main

func main() {
	defer func() {
		if recover() != nil {
			panic("non-nil recover")
		}
	}()
	panic(nil)
}

In the event of a panic, inside the nested functions defer will work as expected, for example:

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func NestedPanic(state *State) {
	state.Append("0 level")
	defer func() {
		if err := recover(); err != nil {
			if errorMessage, ok := err.(string); ok {
				state.Append("0 level — recover string panic: " + errorMessage)
			} else {
				state.Append("0 level — recover panic")
			}
		} else {
			state.Append("0 level — without panic")
		}
	}()

	NestedPanic1Level(state)
}

func NestedPanic1Level(state *State) {
	state.Append("1 level")
	defer func() {
		state.Append("1 level — defer")
	}()

	NestedPanic2Level(state)
}

func NestedPanic2Level(state *State) {
	state.Append("2 level")
	defer func() {
		state.Append("2 level — defer")
	}()

	panic("2 level — panic")
}

func TestNestedPanic(t *testing.T) {
	var state = new(State)

	NestedPanic(state)

	assert.Equal(
		t,
		[]string{
			"0 level",
			"1 level",
			"2 level",
			"2 level — defer",
			"1 level — defer",
			"0 level — recover string panic: 2 level — panic",
		},
		state.Values(),
	)
}

The panic inside the defer

Let’s take a look at the situation when a panic occurs again during a panic recovery.

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func PanicInsideRecover(state *State) {
	state.Append("first")
	defer func() {
		if err := recover(); err != nil {
			if errorMessage, ok := err.(string); ok {
				state.Append("first defer — recover string panic: " + errorMessage)
			} else {
				state.Append("first defer — recover panic")
			}
		} else {
			state.Append("first defer — without panic")
		}
	}()

	state.Append("second")
	defer func() {
		if err := recover(); err != nil {
			if errorMessage, ok := err.(string); ok {
				state.Append("second defer — recover string panic: " + errorMessage)
			} else {
				state.Append("second defer — recover panic")
			}
		} else {
			state.Append("second defer — without panic")
		}

		panic("inside defer")
	}()

	panic("catch me")
}

func TestPanicInsideRecover(t *testing.T) {
	var state = new(State)

	PanicInsideRecover(state)

	assert.Equal(
		t,
		[]string{
			"first",
			"second",
			"second defer — recover string panic: catch me",
			"first defer — recover string panic: inside defer",
		},
		state.Values(),
	)
}

It is expected to be recovered in the next defer with recover in the current goroutine. 

Panic and signature function

If the result returns to the function body and it is different from the function signature, Golang will report an error during the compilation. 

func ReturnSignatureIntEmptyBody() int {

}

func ReturnSignatureNamedIntEmptyBody() (result int) {

}

func ReturnSignatureEmptyIntBody() {
	return 0
}

It is easier to see this in the examples where there will be error messages:

func ReturnSignatureIntPanicBody() int {
	panic("implement me")
}

This example with panic is compiled successfully:

Accordingly, panic can be used while the structure of the program is being built. And only then make an implementation.

Epilogue and features

Everything described above is an example of the code. Tests are expected for those who are already familiar with Go. Also, the question «What is the purpose of this article?» is expected. The first reason is to put examples of panic, defer, and recover together and test them. The second reason is not to forget about their weaknesses. 

Recover only for current Goroutine

If we take an example, where panic occurs in another goroutine without recover, then the program will complete its execution (and report about panic).

package go_defer_reserach

import (
	"sync"
	"time"
)

type PanicFunctionState struct {
	Completed bool
}

func InsideGorountinePanic(state *PanicFunctionState, n, after int) {
	var wg = new(sync.WaitGroup)

	for i := 1; i <= n; i++ {
		wg.Add(1)

		go func(i int) {
			defer wg.Done()

			panicAfterN(i, after)
		}(i)
	}

	wg.Wait()

	state.Completed = true

	return
}


func panicAfterN(i, after int) {
	time.Sleep(time.Millisecond)

	if i%after == 0 {
		panic("i%after == 0")
	}
}
package main

import (
	"fmt"
	go_defer_reserach "gitlab.com/go-yp/go-defer-reserach"
)

func main() {
	var state = new(go_defer_reserach.PanicFunctionState)

	defer func() {
		fmt.Printf("panic state `%t` after\n", state.Completed)
	}()

	fmt.Printf("panic state `%t` before\n", state.Completed)

	go_defer_reserach.InsideGorountinePanic(state, 25, 20)
}

After running this example 10+ times, we mainly received: 

panic state `false` before
panic: i%after == 0

and 1-2 times received

panic state `false` before
panic state `true` after
panic: i%after == 0

In this code example with defer:

wg.Add(1)

go func(i int) {
    defer wg.Done()

    panicAfterN(i, after)
}(i)

no significant advantage compared to code without defer:

wg.Add(1)
go func(i int) {
    panicAfterN(i, after)

    wg.Done()
}(i)

Although the code with defer is slower (up to Go 1.14), the gain of ~ 100 nanoseconds is small compared to the fact that parallel tasks can be performed in milliseconds.

os. Exit terminates the program immediately and ignores defer:

package main

import (
	"fmt"
	"os"
)

// os.Exit ignore defer, output will "first call"
func main() {
	fmt.Println("first call")

	defer fmt.Println("first defer call")

	os.Exit(0)
}

As we expected the first call.

When recover is not working in the current goroutine.

go func(i int) {
	defer wg.Done()
	defer recover()

	panicAfterN(i, after)
}(i)

And so it works, as we expected:

go func(i int) {
	defer wg.Done()
	defer func() {
		recover()
	}()

	panicAfterN(i, after)
}(i)

Thank you for your attention!

If you have any questions or are interested in Go app development, please contact us.

P.S This article weer written as the continuation of Defer, Panic, and Recover. If you want to check the examples, you can check the repository.

Ready to Join Our
Satisfied Clients?
Get started on your IT development journey with itAdviser today. Reach out to us now to learn more about our services.
Let’s talk
Let’s talk