errdefer и безопасный откат

Осваиваем errdefer — откат ресурсов именно тогда, когда что-то пошло не так.

errdefer — отложенное выражение, которое выполняется при выходе из блока только из-за ошибки; идеально для отката частично выполненной инициализации без утечек.

Представьте функцию, которая выделяет три ресурса по очереди. Если третий выделить не удалось, первые два нужно освободить, иначе утечка. В C это превращается в лесенку goto cleanup. Zig решает задачу элегантно через errdefer.

defer против errdefer

Мы уже знаем defer: он выполняется при любом выходе из блока. errdefer — его условный вариант: он срабатывает только если блок завершается из-за возвращённой ошибки. Если функция завершилась успешно — errdefer не выполняется.

fn createUser(alloc: Allocator) !*User {
    const user = try alloc.create(User);
    errdefer alloc.destroy(user); // освободим, только если дальше ошибка

    user.name = try alloc.dupe(u8, "Аня");
    errdefer alloc.free(user.name);

    user.id = try generateId(); // если здесь ошибка —
                                // сработают оба errdefer в обратном порядке
    return user; // успех: ни один errdefer не выполнится
}

Логика читается линейно. После каждого успешного выделения сразу ставится errdefer на его освобождение. Если последующий шаг падает с ошибкой, все накопленные errdefer выполняются в обратном порядке — ресурсы аккуратно откатываются. При успехе же управление доходит до return, и ни один errdefer не срабатывает: ресурсы остаются у вызывающего.

Порядок выполнения

И defer, и errdefer выполняются в порядке, обратном объявлению (как стек). Это естественно: последний выделенный ресурс освобождается первым, повторяя последовательность захвата в обратную сторону.

Как работает под капотом

Компилятор отслеживает, по какому пути управление покидает блок. Для errdefer он вставляет код освобождения только в ветки, где возвращается ошибка. На успешном пути этого кода нет вовсе — нулевые накладные расходы. Это и есть явное, предсказуемое управление ресурсами: вы видите каждое освобождение в коде, а компилятор гарантирует, что оно сработает на нужных путях.

Частые ошибки

Первая — поставить defer вместо errdefer там, где ресурс должен пережить успешный возврат: тогда вы освободите то, что вернули, и получите висячий указатель. Вторая — забыть errdefer после успешного выделения и получить утечку при ошибке на следующем шаге. Третья — ставить errdefer до самого выделения: откатывать ещё нечего.

Итог

  • errdefer выполняется только при выходе из блока из-за ошибки.
  • Идиома: после каждого успешного выделения сразу ставить errdefer на откат.
  • При успехе errdefer не срабатывает — ресурс достаётся вызывающему.
  • Выполняются в обратном порядке объявления, как стек; на успешном пути — нулевые расходы.
Проверьте себя
1. Когда выполняется errdefer?
AПри любом выходе из блока
BТолько если блок завершается из-за возвращённой ошибки
CТолько при успешном завершении
DВ начале блока
2. Почему для ресурса, который возвращается вызывающему при успехе, нельзя ставить defer на освобождение?
Adefer не работает с ресурсами
Bdefer освободит ресурс даже при успехе — вызывающий получит висячий указатель
Cdefer медленнее errdefer
DТак требует синтаксис