Використання 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 {
 map data[string]map[string]float64
 m sync.RWMutex
}

func (s *CurrencyRateService) Update(map data[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(map data[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() (int result) {
 result = 1

return
}

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

func RewriteReturnOne() (int result) {
 defer func() {
 result = 2
}()

 result = 1

return
}

func TestRewriteReturnOne(t *testing.T) {
 assert.Equal(t, 2, RewriteReturnOne())
}
func RewriteReturnOneWithoutAssign() (int result) {
 defer func() {
 result = 3
}()

 return 1
}

func TestRewriteReturnOneWithoutAssign(t *testing.T) {
 assert.Equal(t, 3, RewriteReturnOneWithoutAssign())
}

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

func ModifyReturnOneWithoutAssign() (int result) {
 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(string value) *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(string str) (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, int count) []byte {
 if count == 0 {
 return []byte{}
}
 // Since we cannot return an error on overflow,
 // we should panic repeat if the 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(string str) *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() (int result) {

}

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(int i) {
 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
", state.Completed)
}()

 fmt.Printf("panic state `%t` before
", 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(int i) {
 defer wg.Done()

 panicAfterN(i, after)
}(i)

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

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

wg.Done()
}(i)

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

os.Exit завершує програму відразу та ігнорує defer:

package main

import (
"fmt"
"os"
)

// os.Exit ignore defer, will output "first call"
func main() {
 fmt.Println("first call")

 defer fmt.Println("first defer call")

os.Exit(0)
}

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

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

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

 panicAfterN(i, after)
}(i)

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

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

 panicAfterN(i, after)
}(i)

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

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

Опубліковано: 14/02/20 @ 11:00
Розділ Різне

Рекомендуємо:

Розробка opensource - та приватність Composer-пакетів: як це робити і навіщо
Як подружити розробника і менеджера
Маніпулюємо користувачами: інстинкти
Набір на 5 потік мого курсу SEO Шаолінь
$2000 за рекомендацію та робота в оточенні друзів. Як працюють реферальні програми в ІТ