Красавица и чудовище. Обработка ошибок в Go
Роб Пайк сказал об ошибках в го:
Explicit error checking forces the programmer to think about errors — and deal with them — when they arise.
Это правда. Но не вся.
Допустим, я хочу прочитать список целых чисел из текстового файла. Вот как можно сделать это на питоне:
def read_numbers(filename):
return [int(line.strip()) for line in open(filename)]
Или, в более процедурном стиле:
def read_numbers(filename):
numbers = []
for line in open(filename):
num = int(line.strip())
numbers.append(num)
return numbers
>>> read_numbers("numbers.txt")
[11, 33, 71]
Какой прекрасный, лаконичный, понятный код, не правда ли? Сделаем то же самое в го:
func readNumbers(filename string) ([]int, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var numbers []int
scanner := bufio.NewScanner(file)
for scanner.Scan() {
number, err := strconv.Atoi(scanner.Text())
if err != nil {
return nil, err
}
numbers = append(numbers, number)
}
if err := scanner.Err(); err != nil {
return nil, err
}
if err := file.Close(); err != nil {
return nil, err
}
return numbers, nil
}
Плохо / хорошо
Что плохо в этом коде? (помимо того, что он прямо умоляет разбить на несколько функций вместо одной жирной)
- Он уродливый.
- Тяжело понять происходящее, потому что обработка ошибок перемешана с основной логикой.
А вот что хорошо:
- Видны все ситуации, в которых что-то может пойти не так.
- Пока пишешь код, го заставляет решить, что делать с каждой ошибкой.
Серьёзный плюс, на самом деле. Необработанные ошибки создают огромное количество багов в программах на других языках. Го же тычет программисту в лицо ошибками и требует явно указать, что с ними делать.
Но всегда ли это плюс? Обычно обработка ошибки происходит не там, где ошибка выброшена, а на более высоком уровне. Часто — на значительно более высоком. В го это приводит к постоянным «пробросам» ошибок наверх:
if err != nil {
return err
}
Такой конструкцией переполнена любая программа на го. Ценность её нулевая, потому что это чисто техническая передача ошибки выше, выше и выше — до момента, когда она будет обработана.
В языках с конструкцией try-catch
такой костыль не нужен — достаточно поставить catch
на том уровне, где ошибка будет обработана. При этом на более низких уровнях можно писать код так, как будто никаких ошибок нет — это делает программу проще для понимания и модификации.
На питоне я могу сделать так:
try:
numbers = read_numbers("numbers.txt")
except Exception as exc:
print(f"Failed to read numbers: {exc}")
И не заботиться внутри read_numbers()
об обработке ошибок. Если конкретная причина ошибки не важна, это нормальный подход.
Го же вынуждает программиста возиться с каждой ошибкой — даже когда не надо. Роб Пайк сделал выбор за вас, живите с этим.
Что дальше
Думаю, авторы языка понимали лукавство утверждений о великолепии обработки ошибок в го. Именно этим можно объяснить их предложение добавить в Go 1.14 функцию try.
Вместо:
f, err := os.Open(filename)
if err != nil {
return ..., err
}
Можно было бы писать так:
f := try(os.Open(filename))
Сложно представить себе более ужасное решение. Оно несколько повышает читабельность кода, но полностью убивает то единственное, что есть хорошего в работе с ошибками в го — тот самый «explicit error checking», который «forces the programmer to think about errors».
К счастью, волна недоумения от сообщества заставила авторов передумать:
We still believe that error handling in Go is not perfect and can be meaningfully improved, but it is clear that we as a community need to talk more about what specific aspects of error handling are problems that we should address.
Аминь.
P.S. Результаты голосования:
Подписывайтесь на канал, чтобы не пропустить новые заметки 🚀