После изучения данного урока вы сможете:

  • Записывать файлы и справляться с неудачами;
  • Креативно исправить ошибки;
  • Создавать и идентифицировать определенные ошибки;
  • Хранить спокойствие и продолжать действовать.

Слышатся сирены. Студенты и учителя быстро выскочили из классов и столпились у точки сбора. Никакой опасности в поле зрения нет и ничего не горит. Это очередная учебная пожарная тревога. В случае реальной чрезвычайной ситуации все точно будут знать, что делать.

Форум Гоферов

Мы работаем над форумом для программистов на Golang. Очень нужны модераторы которые хотят помочь с ответами для новичков и помочь в развитии Go-сообщества.

Go на Форум

Уроки, статьи и Видео

Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.

Go в ВК Go в Telegram

Содержание статьи

Файл не найден, неверный формат, сервер недоступен. Что делает программа, когда что-то идет не так? Возможно, проблему можно решить, и тогда операции будут выполняться должным образом. Иногда лучше всего просто выйти и закрыть двери — или на крайний случай разбить окно и выскочить наружу.

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

В данном уроке будут рассмотрены способы исправления ошибок и выяснения причины их появления. Под конец сравним стиль исправления ошибок Go и в других языках программирования.

В начала 18 века английский поэт Александр Поуп написал поэму, строчка которой известна по сей день: to err is human, то есть человеку свойственно ошибаться. Подумайте, как данную строку можно сравнить с программированием.

To err is human; to forgive, divine.

Александ Поуп, “An Essay on Criticism: Part 2”

Все делают ошибки. Системы не исключение. Ошибки повсеместны. Они не считаются редким явлением, поэтому лучше быть готовым. Принимайте ошибки, не игнорируйте их. Работайте над исправлением и двигайтесь дальше.

Исправление ошибок в Golang

В языках программирования прошлого лимитация на одно возвращаемое значение делали исправление ошибок не совсем понятным. Функции переполняли бы одинаковое возвращаемое значение для указания как ошибки, так и успешного значения, или запрашивали бы побочный канал для обращения к ошибки вроде глобальной переменной errno. Что еще хуже, механизм сообщения об ошибках было непоследовательным от функции к функции.

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

Чтобы продемонстрировать обработку ошибок, Листинг 1 вызывает функцию ReadDir. Если возникает ошибка, переменная err не будет равна nil, что заставит программу вывести ошибку и немедленно завершить работу. Ненулевое значение, переданное os.Exit, сообщает операционной системе, что произошла ошибка.

Если ReadDir успешно выполнена, files будет назначен к срезу os.FileInfo, предоставляющий информацию о файлах и каталогах по указанному пути. В данном случае точка уточняет путь, указывающий текущую директорию.

Когда возникает ошибка, не стоит полагаться на другие возвращаемые значения. Они могут быть нулевыми для своего типа, но некоторые функции возвращают данные частично или же совершенно иные значения.

При запуске Листинга 1 на Go Playground в выводе будет список директорий:

Для создания списка содержимого другой директории замените текущую директорию (".") в Листинге 1 названием другой директории вроде "etc". Список может содержать как файлы, так и директории. Вы можете использовать file.IsDir() для того, чтобы различить их.

Вопросы для проверки:

  1. Переделайте Листинг 1 для чтения воображаемой директории под названием "unicorns". Какая ошибка выйдет?
  2. Какое сообщение об ошибке выйдет при использовании ReadDir над файлом "/etc/hosts" вместо директории.

Элегантная обработка ошибок в Golang

Разработчикам Go рекомендуется учитывать и обрабатывать любые ошибки, которые возвращают функции. Количество кода для обработки ошибок увеличивается довольно быстро. К счастью, есть несколько способов уменьшить размер кода обработки ошибок без ущерба надежности.

Некоторые функции выполняют вычисления, преобразования данных и другую логику, где ошибки будут некстати. Есть функции, которые взаимодействуют с файлами, базами данных и серверами. Связь несколько ненадежна и может потерпеть неудачу. Одной из стратегий уменьшения кода обработки ошибок является изоляция безошибочного подмножества программы от изначально подверженного ошибкам кода.

Но как насчет кода, который возвращает ошибки? Мы не можем удалить ошибки, но можем упростить код обработки ошибок. Чтобы продемонстрировать это, напишем небольшую программу для записи в файл следующих английских слоганов Go (Go Proverbs), а затем улучшим обработку ошибок, пока код не станет приемлемым.

Errors are values.
Don’t just check errors, handle them gracefully.
Don’t panic.
Make the zero value useful.
The bigger the interface, the weaker the abstraction.
interface{} says nothing.
Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.
Documentation is for users.
A little copying is better than a little dependency.
Clear is better than clever.
Concurrency is not parallelism.
Don’t communicate by sharing memory, share memory by communicating.
Channels orchestrate; mutexes serialize.

Rob Pike, Go Proverbs

Запись данных в файле

При записи файла множество вещей может пойти не так. Если путь неправильный, или есть проблемы с разрешением, столкнуться с ошибкой можно еще перед началом записи. По окончании записи на диске устройства может закончится место, или же съемный диск может быть случайно извлечен. В дополнении ко всему в конце файл должен быть закрыт, это позволяет убедиться, что все успешно записано на диск, а также избежать утечки информации.

На заметку: Операционная система ограничивает число открытых файлов, поэтому при открытии каждого нового файла память уменьшается. Когда файл не используется, но при этом остается открытым, расход ресурсов является примером утечки.

Главная функция в Листинге 2 вызывает proverbs для создания файла и обрабатывает любые ошибки, отображая ее и затем выходя. Другая имплементация может обрабатывать ошибки иначе, возможно, подсказывая пользователю другой путь или название файла. Хотя функция proverbs может быть написана так, чтобы она выходила при возникновении ошибок, полезно разрешить вызывающему элементу решать, как обрабатывать ошибки.

Функция proverbs может вернуть error, что является специальным встроенным типом для ошибок. Функция повременит с созданием файла из за ошибки. Если в данный момент возникает ошибка, нет нужды закрывать файл, поэтому процесс обрывается. Оставшаяся часть функции записывает строки в файл и гарантирует, что файл закрыт, независимо от его успешности, как показано в коде ниже.

В предыдущем коде много моментов для обработки ошибок — так много, что запись каждого выражения из «Go Proverbs» может стать довольно утомительной.

Плюсом является тот факт, что у кода для обработки ошибок есть отступ, это позволяет легче ориентироваться в коде. Отступы для ошибок являются общим паттерном Go, что во время имплементации можно усовершенствовать.

Вопрос для проверки:

Почему функции должны возвращать ошибку вместо выхода из программы?

Ключевое слово defer в Golang

Убедиться, что файл правильно закрыт, можно с помощью ключевого слова defer. Go гарантирует, что все отложенные действия будут выполнены до возврата содержащей функции. В
следующем листинге каждый возвращаемый оператор, следующий за defer, приведет к вызывающему методу f.Close().

На заметку: Поведение предыдущего кода похоже тому, что в Листинге 3. Изменение кода без изменения его поведения называется рефакторингом. Как и переосмысление первого черновика сочинения, рефакторинг является важным навыком для написания лучшего кода.

Вы можете отложить любую функцию или метод, и как множество возвращаемых значений, отсрочка нужна не для уточнения обработки ошибки. Она улучшает обработку ошибок, избавляясь от необходимости постоянно помнить об очистке. Благодаря defer, код для обработки ошибок может сфокусироваться только на своей задачи и больше ни о чем.

Ключевое слово defer облегчает процесс, однако проверка на наличие ошибок после каждой строки кода очень утомительна. Пришло время для креативности!

Вопрос для проверки:

Когда будет вызвано отсроченное действие?

Креативная обработка ошибок в Golang

15 января 2015 года в блоге Go была опубликована отличная статья об обработке ошибок. В статье описывается простой способ для записи в файл без повторения одинакового кода для обработки ошибок после каждой строчки.

Для применения данной техники вам понадобится объявить новый тип, что вызывается в Листинге 5. Если при записи safeWriter в файл возникает ошибка, он сохраняет ошибку вместо ее возвращения. Следующие попытки записи в одинаковый файл будут пропущены, если writeln видит, что раньше была ошибка.

Через использование safeWriter следующий листинг записывает несколько строк без репетативной обработки ошибок, но по-прежнему возвращает все возникшие ошибки.

Это более простой и чистый способ для записи текстового файла, но смысл не в этом. Такая же техника может использоваться для создания zip-файлов или для совершенно разных задач. Крупная идея становится значительнее одной техники:

… ошибки являются значениями, и сила языка программирования Go в том, что он может обработать их.

Роб Пайк, «Ошибки — это значения«

Перед вами элегантный способ обработки ошибок в Go.

Вопрос для проверки:

Если бы сообщение об ошибке в Листинге 6 сообщало файлу “Clear is better than clever.”, какие бы событие последовали за этим?

Новые ошибки в программе на Golang

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

Для демонстрации новых ошибок Листинг 7 создает основу для Судоку, что представляет собой сетку 9 на 9. Каждый квадрат сетки может содержать цифру от 1 до 9. Имплементация использует массив с фиксированным размером, ноль указывает на пустой квадрат.

Пакет errors содержит функцию конструктора, что принимает строку для сообщения об ошибке. Используя ее, метод Set в «Листинге 8» может создать и возвратить ошибку "out of bounds".

Проверка параметров в начале метода защищает оставшуюся часть метода от неправильного ввода.

Функция inBounds в следующем листинге помогает убедиться, что row и column находятся в пределах границ сетки. Она не дает методу Set забиться лишними деталями.

Наконец функция main в следующем листинге создает сетку и отображает любую ошибку, возникшую в результате неправильной замены.

На заметку: Для сообщений ошибок часто используются части предложений, чтобы перед отображением к ним можно было добавить дополнительный текст.

Всегда читайте сообщения об ошибках. Рассматривайте их как часть пользовательского интерфейса программы, будь он для конечных пользователей и разработчиков. Фраза «out of bounds» неплоха, но более точное «outside of grid boundaries» может быть лучше. А сообщение «error 37» вообще ни о чем не говорит.

Вопрос для проверки:

В чем преимущество защит от плохого ввода перед функцией?

Причины каждой ошибки в Go

Многие пакеты Go объявляют и экспортируют переменные для ошибок, которые они могут вернуть. Для использования этого с сеткой Судоку следующий листинг объявляет две переменные для ошибок на уровне пакета.

На заметку: Принято присваивать сообщения об ошибках переменным, что начинаются со слова Err.

По объявлении ErrBounds вы можете изменить метод Set для возвращения его вместо создания новой ошибки, как показано в следующем коде.

Если метод Set возвращает ошибку, вызывающая сторона может различить возможные ошибки и обрабатывать определенные ошибки по-разному, как показано в следующем листинге. Вы можете сравнить ошибку, возвращаемую с переменными ошибки, используя == или оператор switch.

На заметку: Конструктор errors.New имплементируется через использование указателя, поэтому оператор switch в предыдущем примере сравнивает адреса памяти, текст не содержит сообщения об ошибке.

Задание для проверки:

Напишите функцию validDigit и используйте ее, чтобы убедиться, что метод Set принимает только цифры между 1 и 9.

Настраиваемые типы ошибок в Golang

Каким бы полезным не был errors.New, иногда нужно, чтобы ошибки описывались не просто сообщением. Go достаточно свободен в этом плане.

Тип error является встроенным интерфейсом, как показано в следующем примере. Любой тип, что имплементирует метод Error() для возвращения строки, неявно удовлетворяет интерфейс. В качестве интерфейса возможно создать новые типы ошибок.

Множество ошибок в Golang

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

Вместо возвращения одной ошибки за раз, метод Set может сделать несколько проверок и вернуть все ошибки сразу. Тип SudokuError в Листинге 15 является срезом error. Он удовлетворяет интерфейсу error с методом, что соединяет ошибки вместе в одну строку.

На заметку: Принято, что настраиваемые типы ошибок вроде SudokuError заканчиваются словом Error. Иногда это просто слово Error вроде url.Error из пакета url.

Чтобы использовать SudokuError, метод Set можно модифицировать для валидации границ и цифр, возвращая обе ошибки сразу, как показано в следующем примере.

Если ошибок нет, метод Set возвращает nil. Это не изменилось по сравнению с Листингом 8, но важно отметить, что пустой срез errs здесь не возвращается. Для подробностей можете почитать об интерфейсах nil.

Сигнатура метода для Set также не изменилась по сравнению с Листингом 8. Всегда используйте тип интерфейса error при возвращении ошибок, а не конкретные типы вроде SudokuError.

Вопрос для проверки:

Что произойдет, если метод Set успешно вернет пустой срез errs?

Утверждение типа в Go

Так как Листинг 16 конвертирует SudokuError в тип интерфейса error перед его возвращением, может возникнуть вопрос, как получить доступ к отдельным ошибкам. Решением станет утверждение типа, или type assertion. Используя утверждение типа, вы можете конвертировать интерфейс в конкретный базовый тип.

Утверждение типа в Листинге 17 утверждает err для типа SudokuError через код err.(SudokuError). Это так и есть, то ok будет истинным, а err будет SudokuError, давая доступ к срезам ошибок в данном случае. Помните, что отдельные ошибки для SudokuError являются переменными ErrBounds и ErrDigit, что могут сравниваться в случае необходимости.

В выводе предыдущего кода будут следующие ошибки:

На заметку: Если тип удовлетворяет нескольким интерфейсам, утверждение типа также может конвертировать из одного интерфейса в другой.

Вопрос для проверки:

Что делает утверждение типа err.(SudokuError)?

Принцип работы panic в Golang

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

Есть ли исключения в Golang?

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

Если функция выдает исключение, и никто не собирается его перехватить, это исключение доходит до вызывающей функции, затем до вызывающей ту функцию и так далее, пока достигает вершины стека вызовов (например, функция main).

Исключения — это стиль обработки ошибок, который можно считать включенным. Часто они не занимают код, тогда как выбор обработки исключений может привлекать изрядное количество специализированного кода. Это связано с тем, что вместо использования существующих возможностей языка исключения обычно имеют специальные ключевые слова, такие как try, catch, throw, finally, raise, rescue, except и так далее.

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

Вопрос для проверки:

В чем преимущество значений ошибок Go по сравнению с исключениями?

Как использовать panic в Golang

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

При осознании того, что, отправившись в поездку, вы забыли полотенце,  вы можете запаниковать. Аргумент, переданный panic, может быть любого типа, не только строкой, как показано ниже:

На заметку: Хотя значения ошибок обычно предпочтительнее panic, panic часто лучше, чем os.Exit в том, что panic запустит любую отсроченную функцию, а os.Exit этого делать не станет.

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

Вопрос для проверки:

Как программа может использовать panic?

Тонкости работы с panic в Golang

Чтобы panic не привел к сбою программы, Go предоставляет функцию recover, что показано в Листинге 18.

Отсроченные функции выполняются перед возвращением функции, даже в том случае, если задействуется panic. Если отсроченная функция вызывает recover, panic остановится, и программа продолжит выполняться. В таком случае цель у recover похожа на catch, except и rescue в других языках.

Данный код использует анонимную функцию.

Вопрос для проверки:

Где может использоваться встроенная функция recover?

Заключение

  • Ошибки являются значениями, что внутренне оперируют с несколькими возвращаемыми значениями и другой частью языка Go;
  • Будучи креативным, можно найти множество способов для обработки ошибок;
  • Настраиваемые типы ошибок могут удовлетворить интерфейсу error;
  • Ключевое слово defer помогает выполнить очистку перед возвращением функции;
  • Утверждение типа может конвертировать интерфейс в конкретный тип или другой интерфейс;
  • Не паникуйте — изучите ошибку.

Итоговое задание для проверки:

В стандартной библиотеке Go есть функция для парсинга веб адресов (см. golang.org/pkg/net/url/#Parse). Отобразите ошибку, которая возникает, когда url.Parse используется для неправильного веб адреса вроде того, что содержит пробелы: https://a b.com/.

Используйте специальный символ %#v с Printf для изучения ошибки. Затем выполните утверждение типа *url.Error для получения доступа и вывода полей базовой структуры.

На заметку: URL, или Uniform Resource Locator — адрес страницы в Интернете.

Понравилась статья?

Поддержи наш проект, чтобы мы могли создать больше хорошего контента!