defer: порядок выполнения и отличие ожидаемой ошибки от panic
Содержание курса
defer как гарантия финального действия при любом пути выхода
Представь функцию, которая проверяет входные данные и может вернуться из нескольких точек:
func process(name string) error {
defer fmt.Println("process завершён:", name)
if name == "" {
return fmt.Errorf("имя не может быть пустым")
}
if len(name) > 50 {
return fmt.Errorf("имя слишком длинное")
}
fmt.Println("обработка:", name)
return nil
}
func main() {
err := process("Alice")
if err != nil {
fmt.Println("ошибка:", err)
}
err = process("")
if err != nil {
fmt.Println("ошибка:", err)
}
}
Вывод:
обработка: Alice process завершён: Alice process завершён: ошибка: имя не может быть пустым
defer зарегистрирован первой строкой тела — ещё до любых проверок. Поэтому он сработает при любом return: и при успешном, и при раннем выходе с ошибкой.
Теперь сравни с наивным альтернативным решением:
func processNaive(name string) error {
if name == "" {
return fmt.Errorf("имя не может быть пустым") // финального сообщения нет
}
fmt.Println("обработка:", name)
fmt.Println("process завершён:", name) // до этой строки при раннем return не доберёмся
return nil
}
При return из-за пустого имени последняя строка просто не выполнится. defer решает именно эту проблему: финальное действие отвязано от конкретного пути выполнения и прикреплено к самому факту выхода из функции.
Поэтому устойчивый паттерн выглядит так: зарегистрируй defer сразу после того, как появился ресурс или началось действие, которое нужно завершить. Тогда не придётся вручную дублировать финальный шаг в каждой ветке.
