После моего последнего поста Правильная обработка ошибок — это сложно у меня была интересная дискуссия в Твиттере и интересные комментарии в разделе комментариев, которые я хочу использовать как возможность для другого поста.

В том предыдущем посте я представил вспомогательный тип под названием SafeCloser, который помогает корректно закрывать io.Closer объекты в Go ровно один раз.

В последующих обсуждениях мне было указано, что os.File может изящно обрабатывать несколько закрытий и что на самом деле ни один код Go на GitHub не паникует при двойном вызове Close.

Для os.File это верно. Сейчас. В версии Go 1.8. Мы не знаем, верно ли это для будущих версий.

И хотя на Github размещено много кода, как насчет всех этих репозиториев проприетарного кода, размещенных внутри компаний? Можете ли вы также предположить, что все они изящно обрабатывают множественные закрытия?

Тот факт, что это не гарантируется ни документацией io.Closer, ни документацией os.File, и, скорее всего, не документацией какого-либо другого объекта ввода-вывода, означает, что вы не можете на это рассчитывать. Да, это маловероятно, но в будущей версии Go метод Close() os.File может вызывать панику при двойном вызове.

И просто чтобы проиллюстрировать, что подобные изменения в новых версиях Go на самом деле не так уж и маловероятны, возьмем, например. Функция NewRequest net/http. У этой функции было критическое изменение в Go версии 1.8, которое не было задокументировано. Что случилось?

В версиях до 1.8, когда вы передавали bytes.Buffer как body в NewRequest, он читал этот буфер только тогда, когда запрос действительно выполнялся. В версии 1.8 буфер считывался уже при вызове NewRequest. Это незначительное изменение, но его достаточно, если ваша логика зависит от того, что буфер читается лениво.

Подобные вещи могут происходить в os.File Close() или других Close() методах.

«Не проблема, — могли бы вы возразить, — тесты поймают». Может быть. Может быть нет. Паника может быть вызвана незаметной ошибкой, которая срабатывает только один раз на миллион. Так что он легко проходит все ваши тесты. К сожалению, согласно закону Мерфи, «все, что может пойти не так, пойдет не так».

Как только ваша производственная система начнет обслуживать 100 запросов в секунду, эта проблема будет возникать примерно каждые 3 часа. Добавьте к этому пару других проблем в вашей производственной системе, и вам предстоит много работы, чтобы выкарабкаться из этой ямы. Лучше иметь такие вкусности, как архиватор логов и визуализатор и быстрый цикл развертывания, иначе это может превратиться в настоящий кошмар.

Для самого io.Closer хотелось бы, чтобы документация обновлялась с

Поведение Close после первого вызова не определено. Конкретные реализации могут документировать собственное поведение.

чтобы быть более конкретным. т.е. либо сказать

Вызывающие не должны вызывать Close более одного раза. В противном случае поведение не определено, реализации могут выбрать панику.

or

Реализации Close должны быть идемпотентными, т. е. вызывающие могут вызывать его столько раз, сколько захотят. После первого вызова он должен быть неактивен.

В конце концов, чем поможет интерфейс, когда контракт не одинаков в разных реализациях? К сожалению, внесение любого изменения приведет к поломке кода, поэтому его трудно внедрить.

По моему опыту, лучше просто избежать любой из этих проблем и использовать такую ​​конструкцию, как SafeCloser или любую другую, которая выполняет то же самое.