Ошибки и defer

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 сразу после того, как появился ресурс или началось действие, которое нужно завершить. Тогда не придётся вручную дублировать финальный шаг в каждой ветке.