Використання Defer у Go
by Ярослав ХарактерникПривіт, мене звуть Ярослав. Уже рік я займаюся 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. Якщо захочете перевірити приклади й тести, то заходьте до репозиторію.