Використання Defer у Go

Привіт, мене звуть Ярослав. Уже рік я займаюся Go-розробкою в компанії Evrius. У цій статті опишу добре відомі приклади використання команди defer у Go та покритикую, коли defer зайвий. Відповідно, початок статті буде розрахований на початківців, а продовження — на вже досвідчених.

Defer і порядок у коді

Defer — команда для відкладеного виконання дії перед завершенням основної функції. Defer схожий на пружину, яка в люту зиму зачиняє відчинені двері.

Популярний приклад, це закриття файлу або закриття з’єднання до БД:

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

	// запис у файл або інші операції

	return nil
}

Ще один приклад для блокування та розблокування:

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]
}

Це актуально в прикладах, складніших за CurrencyRateService, де ліпше:

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 та доступ до результату функції

Для прикладу візьмімо просту функцію, в якої є іменована результатна змінна (named return values):

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

	return
}

використаємо defer, щоб змінити результат:

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())
}

Ці приклади зрозумілі, також defer має доступ до значення, що було встановлене перед поверненням:

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

	return 2
}

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

Порядок виконання defer у функції

Зазвичай у прикладах, щоб показати порядок виконання, використовують fmt.Println для легкого запуску в playground.

Та буде простіше показати порядок виконання, використовуючи тести. Ось приклад:

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

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

	result = append(result, "second")

	return result
}

І тест покаже очікуваний результат:

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

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

			"first defer",
		},
	)
}

Допишемо ще один 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
}

І тест, який покаже, що порядок виконання defer зворотний до їх додавання в список на виконання:

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

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

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

Це як розмотування клубка ресурсів, які залежать від попередніх, або ж LIFO.

Defer та ланцюг викликів методів

Підготуємо структуру для збереження стану:

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
}

Та функцію, що буде використовувати ланцюг викликів:

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")
}

Тест покаже, що тільки останній виклик буде відкладеним:

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",
		},
	)
}

Обернувши у функцію, зробимо відкладено для всіх викликів у ланцюжку:

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 і розрахунок аргументів

Підготуємо лічильник:

import (
	"strconv"
)

type StringCounter struct {
	value uint64
}

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

	var next = c.value

	return strconv.FormatUint(next, 10)
}

Напишімо тест і функцію, щоб показати, що аргументи будуть розраховані відразу:

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(),
	)
}

Дія counter.Next() була виконана відразу, тому «first defer call 2».

Якщо обернути у функцію, то отримаємо очікуваний результат:

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(),
	)
}

Повернення помилок і panic

Під час виконання програми можуть відбуватися різноманітні сподівані помилки, у таких випадках у Golang помилка повертається в результаті функції.

У стандартних Golang-бібліотеках повно прикладів повернення помилок, створення файлу чи запис у файл, або найпростіший приклад:

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)
}

Коли ж немає змоги повернути помилку, а помилка є — то відбувається panic, що може завершити виконання програми.

Як приклад, спроба викликати метод до ініціалізації:

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

Також panic можна викликати в коді за допомогою команди panic.

Як приклад, у стандартному пакеті bytes функція Repeat викликає panic під час перевірки аргументів на коректність.

package bytes

func Repeat(b []byte, count int) []byte {
	if count == 0 {
		return []byte{}
	}
	// Since we cannot return an error on overflow,
	// we should panic if the repeat will generate
	// an overflow.
	// See Issue golang.org/issue/16237.
	if count < 0 {
		panic("bytes: negative Repeat count")
	} else if len(b)*count/count != len(b) {
		panic("bytes: Repeat count causes overflow")
	}

	nb := make([]byte, len(b)*count)
	bp := copy(nb, b)
	for bp < len(nb) {
		copy(nb[bp:], nb[:bp])
		bp *= 2
	}
	return nb
}

Також є функції, що починаються зі слова Must і перетворюють повернення помилки на 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, або відновлення після 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 — вбудована функція для відновлення поточної горутини під час panic, корисна тільки в парі з defer.

Recover повертає значення, передане під час виклику panic або nil.

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

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

	var actual = recover()

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

Ось приклад, який покаже, що під час panic у поточній горутині будуть виконані всі defer, та перший defer зі списку виконання, який має recovery, забере значення, передане в 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(),
	)
}

Щоб ви знали, у panic можна передати nil, але ліпше передавати string або error.

package main

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

У разі виникнення panic, усередині вкладених функцій defer відпрацює сподівано, приклад:

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(),
	)
}

Panic усередині defer

Розгляньмо, що буде, коли під час відновлення після panic знову відбудеться panic:

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(),
	)
}

Сподівано буде відновлений на наступному defer з recover у поточній горутині.

Panic і сигнатура функції

Якщо результат, що повертається в тілі функції, відрізняється від сигнатури функції, то Golang повідомить про помилку під час компіляції.

Простіше побачити це на прикладах, де будуть повідомлення про помилку:

func ReturnSignatureIntEmptyBody() int {

}

func ReturnSignatureNamedIntEmptyBody() (result int) {

}

func ReturnSignatureEmptyIntBody() {
	return 0
}

А цей приклад з panic успішно компілюється:

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

Відповідно, panic можна використовувати під час побудови структури програми, а вже потім робити реалізацію.

Епілог та особливості

Усе, що описано — приклади коду, тести — сподіване для тих, хто вже розробляє на Go, і питання: «А навіщо писати цю статтю?» теж очікуване. Перша причина: приклади з panic, defer, recover часто поверхневі, тому я захотів зібрати їх разом і протестувати. Друга причина в тому, що забувають про слабкі сторони.

Recover тільки для поточної горутини

Якщо взяти приклад, де panic відбудеться в іншій горутині без recover, то програма завершить своє виконання (та повідомить про 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)
}

Запустивши цей приклад 10+ разів, переважно отримував:

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

і 1-2 рази отримував

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

У цьому прикладі коду з defer:

wg.Add(1)

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

    panicAfterN(i, after)
}(i)

жодної значної переваги, порівнюючи з кодом без defer:

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

    wg.Done()
}(i)

І навпаки, хоч код з defer і виконується повільніше (до версії Go 1.14), але виграш у ~100 наносекунд — малий, порівнюючи з тим, що завдання, які розпаралелили, може виконуватися мілісекунди.

os.Exit завершує програму відразу та ігнорує 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)
}

Як і очікували first call

Коли recover не працює в поточній горутині:

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

	panicAfterN(i, after)
}(i)

А так працює, як і сподіваємося:

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

	panicAfterN(i, after)
}(i)

Дякую за увагу!

P. S. Ця стаття написана як продовження вже відомої Defer, Panic, and Recover. Якщо захочете перевірити приклади й тести, то заходьте до репозиторію.

Похожие статьи:
Grammarly приглашает в гости всех, интересующихся продуктовой разработкой. Поговорим о прелестях работы Product Owner’а, об общих проблемах...
Для українських захисників розробили мобільний застосунок та сайт «Мінібібліотека українського військового», де зібрані...
Тренинг, который мы вам предлагаем, является уникальной возможностью научиться разрабатывать программное обеспечение...
Привіт, мої любі сішники! Цього разу пропоную розглянути підходи для роботи з кодом та, як завжди, декілька статей про...
Роботизований протез руки від стартапу з українським корінням Esper Bionics потрапив у рейтинг найкращих винаходів 2022...
Яндекс.Метрика