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