Практическая ситуация: основная операция и закрытие ресурса в defer возвращают ошибки. Как спроектировать функцию, чтобы ошибка основной операции не была потеряна?
Нельзя безусловно присваивать ошибку закрытия именованному результату error: это может заменить более важную ошибку основной операции. Обычно ошибку закрытия возвращают только при успешной основной операции, а при наличии обеих ошибок применяют явную политику — например, объединяют их через errors.Join или сохраняют основную, отдельно регистрируя ошибку очистки.
В Go ошибки являются обычными значениями, а не исключениями. Такой подход делает передачу ошибки явной, но возлагает на разработчика ответственность за корректное сочетание ошибок, возникающих в основной операции и при освобождении ресурсов.
Механизм defer был введён как безопасный способ выполнять очистку при выходе из функции. Однако отложенная функция выполняется перед фактическим возвратом управления вызывающему коду, поэтому она может изменить именованные возвращаемые значения.
Если функция выполняет операцию с файлом, соединением или транзакцией, ошибка может возникнуть как в самой операции, так и при закрытии ресурса. Без продуманной политики одна из ошибок будет потеряна, а вызывающий код получит неполную информацию о сбое.
Особенно опасен безусловный перезаписанный результат: ошибка закрытия может скрыть причину, из-за которой операция действительно завершилась неуспешно. Обратная крайность — всегда игнорировать ошибку очистки — также неверна, если закрытие само по себе важно, например при фиксации буферизованных данных.
При возврате из функции сначала вычисляются возвращаемые значения, затем выполняются отложенные функции. Если результат именованный, defer видит ту же переменную и может изменить значение, которое получит вызывающий код.
Практическая политика обычно выглядит так:
В примере return err подготавливает значение результата, но defer выполняется до выхода из run и заменяет его объединённой ошибкой. При этом errors.Is и errors.As могут находить каждую из исходных ошибок в объединённом результате.
Использовать объединение следует осознанно: оно сообщает вызывающему коду о нескольких независимых причинах, но усложняет текст ошибки и может потребовать от клиентов обработки нескольких причин. Если для контракта важна только причина основной операции, допустимо сохранить её, а ошибку очистки передать в систему наблюдаемости.
Если возвращаемое значение не именованное, обычная отложенная функция не может напрямую изменить уже вычисленное значение результата. Поэтому именованные результаты в таких сценариях удобны, но требуют осторожности: любое присваивание результату из defer должно быть частью явно выбранной политики.
Сервис записывает данные во временный файл, затем закрывает его. Запись завершается ошибкой из-за переполнения диска, а закрытие также возвращает ошибку сброса буфера.
Первый вариант — безусловно вернуть ошибку закрытия. Он прост, но скрывает исходную причину сбоя записи и ухудшает диагностику. Второй вариант — всегда вернуть ошибку записи, игнорируя закрытие; он сохраняет главную причину, но может скрыть проблему с потерей буферизованных данных.
Выбранный вариант — объединить обе ошибки, если обе действительно значимы, и дополнительно записать их контекст в журнал. В результате вызывающий код может проверить каждую причину через errors.Is, а оператор получает полную картину сбоя.
1. Может ли defer изменить результат функции, если в return указано выражение, а не имя результата?
Нет, обычная отложенная функция не изменяет уже вычисленное неименованное возвращаемое значение. Для изменения результата она должна получить доступ к изменяемому состоянию: например, к именованному результату или к объекту, на который указывает возвращаемое значение.
Важно различать вычисление результата и выполнение defer: выражение возврата вычисляется раньше, но при именованном результате отложенная функция может изменить саму переменную результата до фактического выхода.
2. Следует ли всегда объединять ошибку основной операции с ошибкой очистки через errors.Join?
Нет. Объединение оправдано, когда обе ошибки несут самостоятельную диагностическую или прикладную ценность. Если ошибка очистки вторична и не влияет на решение вызывающего кода, часто лучше сохранить основную ошибку, а вторичную отправить в лог, метрику или трассировку.
Выбор зависит от контракта функции: объединение повышает полноту информации, но делает обработку результата сложнее и может изменить пользовательское сообщение ошибки.
3. Что произойдёт, если функция вернёт успешный результат, а Close в defer завершится ошибкой?
Если используется именованный error и defer присваивает ему ошибку закрытия при исходном nil, вызывающий код получит ошибку закрытия. Это важно для ресурсов, где финальная очистка выполняет существенную работу, например сбрасывает буфер или завершает запись.
Если ошибка закрытия не должна менять API-функции, её нельзя автоматически присваивать результату. В таком случае её нужно обработать согласно контракту: зарегистрировать, передать в отдельный канал наблюдаемости или явно проигнорировать с обоснованием.