Використання 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. Якщо захочете перевірити приклади й тести, то заходьте до репозиторію.

Похожие статьи:
Уряд ухвалив постанову про єдину платформу для державних реєстрів. Зараз в Україні їх є близько 450, 80% із них — технологічно застарілі...
Друзья, до конференции AI Ukraine’16 остался один месяц, и мы подготовили мини-интервью cсо спикерами. Уверены, что опыт и рекомендации...
Зараз в українській армії є понад 45 тисяч військовослужбовиць. DOU поспілкувався з ІТ-спеціалістками та зібрав історії шести...
В преддверии праздников мы собрали благотворительные акции от фондов и IT-компаний, к которым могут присоединиться все...
Видання The Washington Post опублікувало історії українських фрилансерів, які працюють на платформі Toptal і закликають...
Яндекс.Метрика