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