Почему «ошибки это значения» в Go

от автора

Недавно я перевёл великолепную статью Роба Пайка «Ошибки это значения», которая не получила должной оценки на Хабре. Что неудивительно, поскольку была размещена в официальном блоге Go и рассчитана на Go программистов. Впрочем, суть статьи не всегда сразу очевидна и опытным программистам. И всё же, я считаю её посыл ключевым для понимания подхода к обработке ошибок в Go, поэтому постараюсь объяснить его своими словами.

Я хорошо помню своё первое впечатление от прочтения этой статьи в момент её выхода. Это было примерно следующее: «Странный пример тут выбран — очевидно же, что с исключениями код будет лаконичней; выглядит как попытка оправдаться, что и без исключений можно как-то сократить». При том что я никогда не был фанатом исключений, пример, который рассматривается в статье, прямо напрашивался на это сравнение. Что хотел сказать Пайк фразой «ошибки это значения» было не очень ясно.

В то же время, я понимал, что упускаю нечто важное, поэтому дал себе немного времени, чтобы впитать прочитанное. И в какой-то момент, вернувшись к статье, понимание пришло само собой.

Go просит программиста относиться к ошибкам также, как и к любому другому коду. Просит не рассматривать ошибки, как некоторую особую сущность, для которой нужен специальный подход/инструмент/паттерн, а трактовать код обработки ошибок также, как бы вы трактовали обычную логику вашей программы. Код обработки ошибки — это не особенная конструкция, это полноценная часть вашего кода, как и все остальные.

Представьте, как бы шел ход ваших мыслей, если бы речь шла об обычных языковых конструкция — переменных, условиях, значениях и т.д. — и примените их к обработке ошибочных ситуаций. Они являются сущностями того же самого уровня, они — часть логики. Вы же не игнорируете возвратные значения функций без особого на то повода, правда? Вы не просите от языка специального способа работы с boolean-значениями, потому что if — это слишком скучно. Вы не вопрошаете в замешательстве «А что мне делать, если я не знаю, что мне делать с результатом функции sqrt на этом уровне вложенности?». Вы просто пишете нужную вам логику.

Ещё раз — ошибки это обычные значения, и обработка ошибок — это такое же обычное программирование.

Давайте попробуем проиллюстрировать это примером, похожим на пример из оригинальной статьи. Допустим, у вас есть задача — «сделать несколько повторяющихся записей, подсчитать количество записанных байт и остановиться после 1024 байт». Вы начнете с примера «в лоб»:

var count, n int n = write(“one”) count += n if count >= 1024 {     return }  n = write(“two”) count += n if count >= 1024 {     return } // etc

play.golang.org/p/8033Wp9xly
Разумеется, вы сразу увидете, что не так с этим кодом и как его можно улучшить. Попробуем, следуя DRY, вынести повторяющийся код в отдельное замыкание:

var count int  cntWrite := func(s string) {   n := write(s)   count += n   if count >= 1024 {     os.Exit(0)   } }  cntWrite(“one”) cntWrite(“two”) cntWrite(“three”)

play.golang.org/p/Hd12rk6wNk
Уже лучше, но всё же далеко от того, чтобы считать это хорошим кодом. Замыкание зависит от внешней переменной и использует os.Exit для выхода, что делает код слишком хрупким… Как будет идти ход мысли в этом случае? Посмотрим на проблему с такой стороны — у нас есть функция, которая делает не только запись, но ещё и хранит состояние и реализует определенную, изолированную, логику. Вполне логично создать отдельный тип writer-а для этого:

type cntWriter struct {     count int     w io.Writer }  func (cw *cntWriter) write(s string) {     if cw.count >= 1024 {         return      }     n := write(s)     cw.count += n }  func (cw *cntWriter) written() int { return cw.count }  func main() {     cw := &cntWriter{}     cw.write(“one”)     cw.write(“two”)     cw.write(“three”)      fmt.Printf(“Written %d bytes\n”, cw.count) }

play.golang.org/p/66Xd1fD8II

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

А теперь просто замените «counter» на «error value» и вы получите практически один-в-один пример из оригинальной статьи. Но обратите внимание, как логично и легко шёл ход мысли по мере решения конкретной задачи. Вы не отвлекались на поиски «специальной» фичи языка для счетчика байт и путей передачи его между вызовами, вы не искали «особенного» способа проверки перехода за 1024 байта — вы просто молча реализовали нужную вам логику наиболее удачным способом.


Эта концепция — ошибки как значения — не просто «другой способ» или «новый дизайн», это концептуальный подход. Он может быть принят легко и естественно, а может казаться чем-то диким и неправильным, в зависимости от вашего предыдущего опыта. Зачастую нужно время, чтобы его осознать.

Безусловно, у такого подхода есть и свои минусы, и свои плюсы, как и во всех других подходах. Мы живем не в черно-белом мире, но подход Go тут является и зрелым и свежим одновременно, он чрезвычайно прост и при этом непривычен для понимания, и требует определенных усилий, чтобы прочувствовать всю мощь. Но, что самое важное — он отлично работает на практике.

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

А теперь попробуйте ещё раз прочесть оригинальную статью — «Ошибки это значения» — но теперь посмотрите на неё с вышеописанной перспективы.

PS. Вариант статьи на английском тут.

ссылка на оригинал статьи http://habrahabr.ru/post/270027/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *