Обработка ошибок в Python и Go: исключения против значений

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

В каждой программе можно встретить функцию, которая делает «много всего». Мой случай: загрузка csv-файла с внешнего источника, его разбор, преобразование данных в другой формат и возврат. Для этого она использует несколько сторонних библиотек. Давайте прикинем, что может произойти:

  • может отвалиться сеть
  • файл будет невалидный или не в той кодировке
  • произойдёт обращение к неверному URL (файл переместили)
  • файл будет настолько большой, что клиент не сможет его прочитать

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

Прикладному коду часто всё равно, что стало причиной ошибки: нерабочая сеть или ошибка в документе. Достаточно знать, что функция выполнилась неудачно, чтобы сообщить об этом кому-нибудь. Так в коде иногда появляется приём «перехватывам всё»:

try:
    data = download_and_parse_csv()
except Exception as e:
    log.error(e)
    return

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

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

Ошибка — такое-же возвращаемое значение функции, как её результат. Она сама по себе не влияет на поток выполнения и не роняет программу. Что с ней делать решает прикладной код: можно самому прервать выполнение, вызвав panic, записать ошибку в лог или пробросить дальше.

data, err := DownloadAndParseCSV()
if err != nil {
    // err здесь — любая ситуация, произошедшая по ходу выполнения
    // её могла вернуть библиотека работы с http или парсер csv
    // DownloadAndParseCSV пробросил её сюда
    log.Error(err)
}

Функция, которая может сломаться последним значением возвращает указатель на специальный тип с интерфейсом error. Если указатель не пустой, это говорит о наличии проблемы. Игнорировать ошибки здесь не принято, поэтому после каждого вызова «опасной» функции следует проверка на err != nil.

func DownloadAndParseCSV() (*CSVData, error) {
    user := GetCurrentUser()
    if user == nil || !user.HasPermission("download_and_parse") {
        return nil, errors.New("Permission deined")
    }

    csvFile, err := DownloadCSV(config.URL)
    if err != nil {
      return nil, err
    }

    buffer, err := csvFile.ReadAll()
    if err != nil {
      return nil, err
    }

    data := &CSVData{}
    err = ParseCSV(CSVData, buffer)
    if err != nil {
      return nil, err
    }

    return CSVData, nil
}

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

Ссылки по теме