Краткое содержание
Предлагается новая конструкция try
, созданная специально для устранения if
-выражений, обычно связанных с обработкой ошибок в Go. Это единственное изменение языка. Авторы поддерживают использование defer
и стандартных библиотечных функций для обогащения или оборачивания ошибок. Это маленькое расширение подходит для большинства сценариев, практически не усложняя язык.
Конструкцию try
просто объяснить, легко реализовать, этот функционал ортогонален другим языковым конструкциям и является полностью обратно-совместимым. Он также является расширяемым, если мы захотим этого в будущем.
Остальная часть этого документа организована следующим образом: после краткого введения, мы приводим определение встроенной функции и объясняем ее использование на практике. Раздел обсуждения рассматривает альтернативные предложения и текущий дизайн. В конце будут приведены выводы и план реализации с примерами и секцией вопросов и ответов.
Введение
На прошлой конференции Gophercon в Денвере, члены команды Go (Russ Cox, Marcel van Lohuizen) представили некоторые новые идеи, как снизить утомительность ручной обработки ошибок в Go (черновик дизайна). С тех пор мы получили огромное количество отзывов.
Как объяснял Russ Cox в своем обзоре проблемы, нашей целью является сделать обработку ошибок более легковесной, снизив объем кода, посвященный именно проверке ошибок. Мы также хотим сделать написание кода обработки ошибок более удобным, повысив вероятность того, что разработчики всё-таки будут уделять время корректности обработки ошибок. В то же время мы хотим оставить код обработки ошибок четко видимым в коде программы.
Идеи, обсуждавшиеся в черновике дизайна, сконцентрированы вокруг нового унарного оператора check
, который упрощает явную проверку значения ошибки, полученной из некоторого выражения (обычно вызова функции), а также декларацию обработчиков ошибок (handle
) и набор правил, соединяющих эти две новые конструкции языка.
Большая часть отзывов, которые мы получили, была сфокусирована на деталях и сложности конструкции handle
, а идея похожего на check
оператора оказалась более привлекательной. Фактически, несколько членов комьюнити взяли идею check
-оператора и расширили её. Вот несколько постов, наиболее похожих на наше предложение:
- Первое письменнное предложение (известное нам) использовать конструкцию
check
вместо оператора было предложено PeterRK в его посте Ключевые части обработки ошибок - Не так давно, Markus предложил два новых ключевых слова
guard
иmust
наряду с использованиемdefer
для оборачивания ошибок в #31442 - Также pjebs предложил конструкцию
must
в #32219
Текущее предложение, хотя и отличается в деталях, было основано на этих трех и в целом на обратной связи, полученной на предложенный в прошлом году черновик дизайна.
Для полноты картины, мы хотим заметить, что еще больше предложений по обработке ошибок могут быть найдены на этой странице вики. Также стоит заметить, что Liam Breck пришел с обширным набором требований к механизму обработки ошибок.
Наконец, уже после публикации данного предложения мы узнали, что Ryan Hileman реализовал try
пять лет назад с помощью инструмента og rewriter и успешно использовал его в реальных проектах. См. (https://news.ycombinator.com/item?id=20101417).
Встроенная функция try
Предложение
Мы предлагаем добавить новый похожий на функцию элемент языка, называющийся try
и вызываемый с сигнатурой
func try(expr) (T1, T2, ... Tn)
где expr
означает выражение входного параметра (обычно вызов функции), возвращающее n+1 значений типов T1, T2, ... Tn
и error
для последнего значения. Если expr
является одинарным значением (n=0), это значение должно быть типом error
и try
не возвращает результат. Вызов try
с выражением, которое не возвращает последнее значение типа error
ведет к ошибке компиляции.
Конструкция try
может быть использована только в функции, возвращающей как минимум одно значение, и последнее возвращаемое значение которой имеет тип error
. Вызов try
в других контекстах ведет к ошибке компиляции.
Вызов try
с функцией f()
как в примере
x1, x2, … xn = try(f())
приводит к следующему коду:
t1, … tn, te := f() // t1, … tn, локальные (невидимые) временные переменные
if te != nil {
err = te // присваиваем te возвращаемому параметру типа error
return // выходим из объемлющей функции
}
x1, … xn = t1, … tn // присваивание остальных значений происходит
// только при отстутствии ошибки
Другими словами, если последнее значение типа error
, возвращенное expr
, равно nil
, то try
просто возвращает первые n значений, удаляя финальный nil
.
Если последнее значение, возвращенное expr
, не является nil
, то:
- возвращаемое значение
error
объемлющей функции (в псевдокоде выше названноеerr
, хотя это может быть любой идентификатор или неименнованное возвращаемое значение) получает значение ошибки, возвращенное изexpr
- происходит выход из объемлющей функции
- если объемлющая функция имеет дополнительные возвращаемые параметры, эти параметры сохраняют те значения, которые в них содержались до вызова
try
. - если объемлющая фунцкия имеет дополнительные неименованные возвращаемые параметры, для них возвращаются соответствующие нулевые значения (что идентично сохранению их исходных нулевых значений, которыми они проинициализированы).
Если try
использован в множественном присваивании, как в примере выше, и обнаружена ненулевая (здесь и далее not-nil — прим.пер.) ошибка, присваивание (пользовательским переменным) не выполняется, и ни одна из переменных в левой части присваивания не меняется. То есть try
ведет себя как вызов функции: его результаты доступны только если try
возвращает управление вызывающей стороне (в отличие от случая с возвратом из объемлющей функции). Как следствие, если переменные в левой части присваивания являются возвращаемыми параметрами, использование try
приведет к поведению, отличающемуся от типичного кода, встречающегося сейчас. Например, если a,b, err
являются именованными возвращаемыми параметрами объемлющей фунции, вот этот код:
a, b, err = f()
if err != nil {
return
}
будет всегда присваивать значения переменным a, b
и err
, независимо от того, вернул ли вызов f()
ошибку или нет. Напротив, вызов
a, b = try(f())
в случае ошибки оставит a
и b
неизменными. Несмотря на то, что это тонкий нюанс, мы считаем, что такие случаи являются достаточно редкими. Если требуется поведение с безусловным присваиванием, необходимо продолжать использовать if
-выражения.
Использование
Определение try
явно подсказывает способы его применения: множество if
-выражений, проверяющих возврат ошибки, могут быть заменены на try
. Например:
f, err := os.Open(filename)
if err != nil {
return …, err // пустые значения или другие возвращаемые параметры
}
может быть упрощено до
f := try(os.Open(filename))
Если вызывающая функция не возвращает ошибку, try
использовать нельзя (см. раздел "Обсуждение"). В этом случае, ошибка должна быть в любом случае обработана локально (т.к. нет возврата ошибки), и в этом случае if
остается подходящим механизмом для проверки на ошибку.
Вообще говоря, нашей целью не является замена всех возможных проверок ошибок на выражение try
. Код, который требует другой семантики, может и должен продолжать использовать if
-выражения и явные переменные со значениями ошибок.
Тестирование и try
В одной из наших более ранних попыток написания спецификации (см. ниже раздел "итерации дизайна"), try
был спроектирован паниковать при получении ошибки в случае использования внутри функции без возвращаемой ошибки. Это позволяло использовать try
в юнит-тестах на базе пакета testing
стандартной библиотеки.
В качестве одного из вариантов, можно в пакете testing
позволить использовать тестовые функции с сигнатурами
func TestXxx(*testing.T) error
func BenchmarkXxx(*testing.B) error
для того, чтобы разрешить использование try
в тестах. Тестовая фунция, возвращающая ненулевую ошибку, будет неявно вызывать t.Fatal(err)
или b.Fatal(err)
. Это небольшое изменение библиотеки, позволяющее избежать потребности в разном поведении (возврат или паника) для try
в зависимости от контекста.
Одним из недостатков этого подхода является то, что t.Fatal
и b.Fatal
не смогут вернуть номер строки, на которой упал тест. Другим недостатком является то, что мы должны как-то изменять и субтесты тоже. Решение этой проблемы является открытым вопросом; мы не предлагаем конкретных изменений в пакете testing
в данном документе.
См. также #21111, где предлагается разрешить фунциям-примерам возвращать ошибку.
Обработка ошибок
Оригинальный черновик дизайна в значительной мере касался языковой поддержки оборачивания (wrapping) или обогощения (augmenting) ошибок. Черновик предлагал новое ключевое слово handle
и новый способ декларации обработчиков ошибок. Эта новая языковая конструкция притягивала проблемы как мух из-за нетривиальной семантики, особенно при рассмотрении ее влияния на поток выполнения. В частности, функционал handle
несчастным образом пересекался с функционалом defer
, что делало новую возможность языка неортогональной всему остальному.
Это предложение сводит оригинальный черновик дизайна к его сути. Если требуется обогащение или оборачивание ошибок, есть два подхода: привязываться к if err != nil { return err}
, либо "объявлять" обработчик ошибок внутри выражения defer
:
defer func() {
if err != nil { // может и не быть ошибки - надо проверить
err = … // обогащение/оборачивание ошибки
}
}()
В данном примере err
является названием возвращаемого параметра типа error
объемлющей фунции.
На практике, мы представляем себе такие функции-хелперы как
func HandleErrorf(err *error, format string, args ...interface{}) {
if *err != nil {
*err = fmt.Errorf(format + ": %v", append(args, *err)...)
}
}
ну или что-то похожее. Пакет fmt
может стать естественным местом для таких хелперов (он уже предоставляет fmt.Errorf
). С использованием хелперов определение обработчика ошибки будет во многих случаях сводится к однострочнику. Например, для обогащения ошибки из функции "copy", можно написать
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
если fmt.HandleErrorf
неявно добавляет информацию об ошибке Такая конструкция довольно легко читается и имеет преимущество в том, что может быть реализована без добавления новых элементов синтаксиса языка.
Основным недостатком такого подходя является то, что возвращаемый параметр ошибки должен быть именованным, что потенциально ведет к менее аккуратным API (см. FAQ на эту тему). Мы верим, что мы привыкнем к этому когда соответствующий стиль написания кода устоится.
Эффективность defer
Важным соображением при использовании defer
как обработчика ошибок является эффективность. Выражение defer
считается медленным. Мы не хотим выбирать между эффективным кодом и хорошей обработкой ошибок. Независимо от данного предложения, команды Go рантайма и компилятора обсуждали альтернативные способы реализации и мы верми, что мы сможем сделать типичные способы использования deferдля обработки ошибок сравнимыми по эффективности с существующим "ручным" кодом. Мы надеемся добавить более быструю реализацию defer
в Go 1.14 (см. также тикет CL 171158, который является первым шагом в этом направлении).
Специальные случаи go try(f), defer try(f)
Конструкция try
выглядит как функция и из-за этого ожидается, что ее можно использовать в любом месте, где допустим вызов фунцкии. Однако если вызов try
использован в выражении go
, всё усложняется:
go try(f())
Здесь f()
выполняется в момет выполнения выражения go в текущей горутине, результаты вызова f
передаются в качестве аргументов try
, который запускается в новой горутине. Если f
возвращает ненулевую ошибку, ожидается, что try
осуществит возврат из объемлющей функции; однако нет никакой функции (и нет никакого возвращаемого параметра типа error
), т.к. код выполняется в отдельной горутине. Из-за этого мы предлагаем запретить try
в go
-выражении.
Ситуация с
defer try(f())
выглядит похоже, но здесь семантика defer
означает, что выполнение try
будет отложено до момента перед возвратом из объемлющей функции. Как и раньше, f()
вычисляется в момент выполнения выражения defer
, и его результаты передаются в отложенный try
.
try
проверяет ошибку, которую вернул f()
, только в самый последний момент перед возвратом из объемлющей функции. Без изменения поведения try
, такая ошибка может перезаписать другое значение ошибки, которое пытается вернуть объемлющая функция. Это в лучше случае запутывает, в худшем — провоцирует ошибки. Из-за этого мы предлагаем запретить вызов try
и в выражении defer
тоже. Мы всегда можем пересмотреть это решение, если найдётся разумное применение такой семантике.
Наконец, как и остальные встроенные конструкции, try
можно использовать только как вызов; его нельзя использовать как функцию-значение или в выражении присваивания переменной как в f := try
(так же как запрещены f := print
и f := new
).
Обсуждение
Итерации дизайна
Ниже идет краткое обсуждение более ранних дизайнов, которые привели к текущему минимальному предложению. Мы надеемся, что это прольет свет на выбранные дизайнерские решения.
Наша первая итерация этого предложения была вдохновлена двумя идеями из статьи "Ключевые части обработки ошибок", а именно использование встроенной функции вместо оператора и обычной Go-функции для обработки ошибок вместо новой языковой конструкции. В отличие от той публикации, наш обработчик ошибок имел фиксированную сигнатуру func(error) error
для упрощения дела. Обработчик ошибок вызывался бы функцией try
при наличии ошибки, перед тем как try
осуществила бы выход из объемлющей функции. Вот пример:
handler := func(err error) error {
return fmt.Errorf("foo failed: %v", err) // оборачиваем ошибку
}
f := try(os.Open(filename), handler) // обработчик будет вызван при ошибке
В то время как этот подход разрешал определение эффективных пользовательских обработчиков ошибок, он также поднимал множество вопросов, которые очевидно не имели корректных ответов: Что должно происходить если в обработчик передан nil? Стоит try
паниковать или расценивать это как отсутствие обработчика? Что если обработчик вызван с ненулевой ошибкой и потом возвращает нулевой результат? Означает ли это что ошибка "отменена"? Или объемлющая функция должна вернуть пустую ошибку? Были также сомнения относительно того, что опциональная передача обработчика ошибки будет подталкивать разработчиков к игнорированию ошибок вместо их корректой обработки. Было бы также легко сделать везде правильную обработку ошибок, но пропустить одно использование try
. И тому подобное.
В следующей итерации возможность передавать пользовательский обработчик ошибок была удалена в пользу использования defer
для оборачивания ошибок. Это казалось лучшим подходом, потому что это делало обработчики ошибок гораздо более заметными в исходном коде. Этот шаг также устранил все вопросы, касающиеся опциональной передачи функций-обработчиков, но потребовал, что возвращаемые параметры с типом error
были именованными, если к ним требовался доступ (мы решили что это норм). Более того, в попытке сделать try
полезным не только внутри функций, возвращающих ошибки, пришлось сделать поведение try
зависящим от контекста: если try
использовался на уровне пакета, или если он был вызван внутри функции, не возвращающей ошибку, try
автоматически паниковал при обнаружении ошибки. (И как побочный эффект, из-за этого свойства конструкция языка была названа must
вместо try
в том предложении.) Контекстно-зависимое поведение try
(или must
) казалось естественным и также довольно полезным: это позволило бы устранить многие пользовательские функции, используемые в выражениях инициализации переменных пакета. Это также открывало возможность использования try
в юнит-тестах с пакетом testing
.
Однако, контексто-зависимое поведение try
было чревато ошибками: например, поведение функции, использующей try
, могло по-тихому меняться (паниковать или нет) при добавлении или удалении возвращаемой ошибки к сигнатуре функции. Это казалось слишком опасным свойством. Очевидным решением было разделить функциональность try
в две отдельные функции must
и try
, (очень похоже на то, как это предлагалось в #31442). Однако это потребовало бы двух встроенных функций, при том что только try
напрямую связана с лучшей поддержкой обработки ошибок.
Поэтому, в текущей итерации, вместо включения второй встроенной функции, мы решили удалить двойственную семантику try
и, следовательно, разрешить ее использование только в функциях, возвращающих ошибку.
Особенности предложенного дизайна
Это предолжение довольно краткое и может казаться шагом назад по сравнению с прошлогодним черновиком. Мы считаем, что выбранные решения оправданы:
Перво-наперво,
try
имеет ровно ту же семантику предложенного в оригинале оператораcheck
при отсутствииhandle
. Это подтверждает верность оригинального черновика в одом из важных аспектов.
Выбор встроенной функции вместо операторов имеет несколько преимуществ. Не требуется нового ключевого слова вроде
check
, которое сделало бы дизайн несовместимым с существующими парсерами. Также нет необходимости в расширении синтаксиса выражений новым оператором. Добавление новой встроенной функции сравнительно тривиально и полностью ортогонально другим возможностям языка.
Использование встроенной функции вместо оператора требует использование скобок. Мы должны писать
try(f())
вместоtry f()
. Это (небольшая) цена, которую мы должны заплатить за обратную совместимость с существующими парсерами. Однако это также делает дизайн совместимым с будущими версиями: если мы решим по дороге, что передавать в каком-то виде функцию обработки ошибок или добавить вtry
дополнительный параметр для этой цели — хорошая идея, добавить дополнительный аргумет в вызовtry
будет тривиально.
Как оказалось, необходимость писать скобки имеет свои преимущества. В более сложных выражениях с несколькими вызовами
try
, скобки улучшают читаемость путем устранения необходимости разбираться с приоритетом операторов, как в следующих примерах:
info := try(try(os.Open(file)).Stat()) // предложенная функция try
info := try (try os.Open(file)).Stat() // приоритет try меньше точки
info := try (try (os.Open(file)).Stat()) // приоритет try выше точки
Вторая строка соответствует оператору try
, который имеет приоритет ниже вызова функции: скобки требуются вокруг всего внутреннего выражения try
, т.к. результат этого try
является ресивером (receiver) вызова .Stat
(вместо результата os.Open
).
Третья строка соответствует оператору try
, который имеет более высокий приоритет чем вызов функции: скобки необходимы вокруг os.Open(file)
т.к. его результат является аргументом для внутерннего try
(мы не хотим, чтобы внутренний try
применялся только к os
, и не хотим, чтобы внешний try
применялся только к результату внутреннего try
).
Первая строка по крайней мере выглядит наименее удивительно и наиболее читаемой, т.к. использует лишь знакомую нотацию вызова функции.
- Остутствие отдельной языковой конструкции для поддержки оборачивания ошибок может некоторых разочаровать. Однако стоит заметить, что данное предложение не исключает добавления подобной конструкции в будущем. Определенно лучше подождать, пока не появится действительно хорошее решение, нежели преждевременно добавлять в язык механизм, который решает не все проблемы.
Выводы
Основное отличие между этим предложением и оригинальным черновиком дизайна состоит в устранении обработчика ошибок как новой языковой конструкции. Получившееся упрощение действительно огромно, при этом нет значительной потери универсальности. Эффект от определения явных обработчиков ошибок может быть достигнут подходящими выражениями defer
, которые также являются заметными в исходном коде в начале тела функции.
В Go встроенные функции являются запасным планом для нетипичных в каком-то смысле операций, которые в то же время не требуют специального синтаксиса. Например, первые версии Go не определяли встроенную функцию append
. Только после ручного определения append
снова и снова для различных типов слайсов стало понятно, что отдельная поддержка со стороны языка тут оправдана. Повторяющиеся реализации помогли прояснить, как именно должна выглядеть такая встроенная функция. Мы считаем, что мы находимся в аналогичной ситуации с try
.
Также может показаться странным влияние встроенных функций на поток исполнения, но мы также должны помнить, что в Go несколько встроенных функций уже занимаются этим: panic
и recover
. Встроенный тип error
и функция try
дополняют эту пару.
В итоге, try
кажется необычным на первый взгляд, но это просто синтаксический сахар, спроектированный для одной конкретной задачи — обработки ошибок без дополнительного кода — и для того, чтобы выполнять эту задачу хорошо. Таким образом он аккуратно вписывается в философию Go:
- Нет интерференции с остальными конструкциями языка
- Так как это синтаксический сахар,
try
легко объяснить в базовых терминах языка - Дизайн не требует нового синтаксиса
- Дизайн является полностью обратно-совместимым
Это предложение не покрывает все варианты обработки ошибок, которые хотелось бы обрабатывать, но оно хорошо решает наиболее распространенные случаи. Для всего остального есть if
-выражения.
Реализация
Для реализации потребуется:
- Дополнить спецификацию Go.
- Научить тайпчекер компилятора обрабатывать
try
. Ожидается, что фактическая реализация будет довольно понятным преобразованием синтаксического дерево во фронтенде компилятора. На стороне бекенда изменений не ожидается. - Научить
go/types
пониматьtry
. Это незначительное изменение. - Поправить соответствующим образом
gccgo
. (Опять же, только фронтенд). - Написать несколько тестов на встроенную функцию.
Так как это обратно-совместимое изменение языка, изменений в в библиотеке не требуется. Однако, мы ожидаем добавление функций для поддержки обработки ошибок. Их детальный дизайн и соответствующая работа по реализации будет обсуждаться отдельно.
Robert Griesemer обновит спецификацию и go/types
, включая дополнительные тесты и (возможно) cmd/compile
. Мы стремимся к тому, чтобы начать реализацию вместе со стартом цикла разработки Go 1.14, около 1 августа 2019.
Независимо, Ian Lance Taylor займется изменениями в gccgo
, выпускаемому по отдельному расписанию.
Как отмечалось в посте "Go 2, мы идем!", циклы разработки являются способом собрать опыт по использованию новых возможностей и обратную связь от ранних пользователей.
1 ноября, к моменту заморозки релиза, мы пересмотрим предложенные изменения и решим, включать их в Go 1.14 или нет.
Примеры
Пример CopyFile
из обзора теперь выглядит так:
func CopyFile(src, dst string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // только если в “try” ниже будет ошибка
}
}()
try(io.Copy(w, r))
try(w.Close())
return nil
}
С использованием хелпера, обсуждавшегося в разделе "обработка ошибок", первый defer
становится однострочником:
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
Все еще можно иметь несколько обработчиков и даже цепочки обработчиков (через стек defer
-ов), но теперь поток выполнения определяется существующей семантикой defer
а не новым незнакомым механизмом, который еще нужно изучать.
Пример printSum
из черновика дизайна не требует обработчика ошибок и становится
func printSum(a, b string) error {
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}
и еще проще:
func printSum(a, b string) error {
fmt.Println(
"result:",
try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
)
return nil
}
Функция main
этой полезной но простенькой программы может быть разделена на две функции:
func localMain() error {
hex := try(ioutil.ReadAll(os.Stdin))
data := try(parseHexdump(string(hex)))
try(os.Stdout.Write(data))
return nil
}
func main() {
if err := localMain(); err != nil {
log.Fatal(err)
}
}
Из-за того что try
требует хотя бы аргумент с ошибкой, его можно использовать для проверки оставшихся необработанными ошибок:
n, err := src.Read(buf)
if err == io.EOF {
break
}
try(err)
Вопросы и ответы
Предполагается, что данный раздел будет обновляться.
В: В чем состоит основная критика оригинального черновика дизайна?
О: Черновик дизайна предлагает два новых ключевых слова check
и handle
, которые исключают обратную совместимость предложения. Более того, семантика handle
была довольно сложной и его функциональность существенно пересекается с defer
, что делает handle
неортогональной возможностью языка.
В: Почему try является встроенной функцией?
О: Для встроенной функции try
не требуется добавления в Go нового ключевого слова или оператора. Добавление нового ключевого слова не является обратно-совместимым изменением языка, потому что ключевое слово может конфликтовать с идентификаторами в существующих программах. Добавление нового оператора требует нового синтаксиса и выбора подходящего оператора, чего мы хотели бы избежать. Использование обычного синтаксиса вызова функции также имеет преимущества, как описано в разделе "Особенности предложенного дизайна". И try
не может быть обычной функцией, т.к. количество и типы его результатов зависят от его входных параметров.
В: Почему try
названо try?
О: Мы рассматривали различные альтернативы, включая check
, must
и do
. Даже хотя try
является встроенной функцией и за счет этого не конфликтует с существующими идентификаторами, такие идентификаторы могут затенять встроенную функцию и этим делать ее недоступной. try
выглядит менее распространенным пользовательским идентификатором чем check
(возможно, потому что является ключевым словом в других языках), и из-за этого имеет меньшую вероятность быть случайно затененным. Также это слово короче и неплохо передает собственную семантику. В стандартной библиотеке мы используем паттерн пользовательских функций must
для возбуждения паники при возникновении ошибки в выражениях инициализации переменных; try
— не паникует. Наконец, и Rust и Swift также используют try
для аннотации явной проверки вызова функции (смотри также следующий вопрос). Разумно использовать то же слово для такой же идеи.
В: Почему мы не можем использовать ?
как в Rust?
О: Go проектировался с сильным упором на читабельность; мы хотим, чтобы даже незнакомые с языком люди могли понимать код на Go (это не подразумевает что каждое имя должно быть самоочевидным; у нас все-таки есть спецификация языка). Пока мы избегали криптографических сокращений или символов в языке, включая необычные операторы вроде ?
, которые имеют неоднозначное или неочевидное значение. В общем смысле, идентификаторы, определенные в языке, являются полными английскими словами (package, interface, if, append, recover, и.т.д.), или сокращениями, если сокращенная версия является однозначной и понятной (struct, var, func, int, len, image, и т.д.). Rust предоставляет оператор ?
для смягчения проблем с try
и цепочками — это гораздо меньшая проблема в Go, где выражения обычно проще, а цепочки (в отличие от вложенности) гораздо меньше распространены. Наконец, использование ?
добавит в язык новый постфиксный оператор. Это потребует нового токена, и нового синтаксиса, и изменения кучи пакетов (сканнеров, парсеров и т.д.) и тулзов. Также будет гораздо труднее реализовывать будущие изменения. Использование встроенной функции устраняет все эти проблемы, сохраняя дизайн гибким.
В: Необходимость именовать последний возвращаемый параметр функции (типа error) для того, чтобы defer мог его увидеть, ломает вывод go doc. Неужели нет лучшего подхода?
О: Мы можем добавить в go doc
распознование специальных случаев, где все возвращаемые результаты кроме финального параметра-ошибки имеют пустое (_
) имя, и пропускать остальные имена возвращаемых параметров для данного случая. Например, сигнатура func f() (_ A, _ B, err error)
будет представляться в go doc
как func f() (A, B, error)
. В конечном счете это вопрос стиля, и мы считаем, что мы к нему привыкнем, как к отсутствию точек с запятой. Иными словами, если мы хотим добавлять больше новых механизмов в язык, существуют разные способы это сделать. Например, можно определить новую, правильно названную встроенную переменную, которая является псевдонимом для последнего возвращаемого параметра-ошибки, возможно видимую только внутри литерала отложенной (deferred) функции. В качестве альтернативы Jonathan Geddes предлагает чтобы вызов try()
без аргументов возвращал указатель на возвращаемый параметр ошибки.
В: Не будет ли использование defer для оборачивания ошибок тормозным?
О: В настоящее время defer
является относительно дорогой операцией в сравнении с обычным потоком выполнения. Однако, мы считаем, что можно сделать основные варианты использования defer для обработки ошибок сравнимыми по производительности с текущим "ручным" подходом. См. также CL 171758, где ожидается увеличение производительности defer примерно на 30%.
В: Не отобьет ли такой подход всю оходу добавлять контекст в ошибки?
О: Мы думаем, информативность ошибок это отдельная от добавления контекста проблема. Контекст, который обычная фунцкия обычно добавляет к собственным ошибкам (в основном, информация о своих аргументах), обычно подходит к множественным проверкам ошибок. План по стимулированию использования defer
для добавления контекста к ошибкам является отдельной головной болью для сокращения кода обработки ошибок, что является целью данного предложения. Дизайн конкретных defer
-хелперов является частью https://golang.org/issue/29934(Значения ошибок в Go 2), а не данного предложения.
В: Последний аргумент, переданный в try, должен иметь тип error. Почему недостаточно, чтобы входящему параметру можно было присвоить ошибку?
О: Распространенной ошибкой новичков является присваивание конкретного нулевого указателя переменной типа error
(который является интерфейсом) только для того, чтобы проверить, что эта переменная не nil
. Ограничение типа входного параметра предотвращает возникновение этой ошибки при использовании try
. (Мы можем пересмотреть это решение в будущем, если потребуется. Ослабление этого правила будет обратно-совместимым изменением).
В: Если бы в Go были дженерики, могли бы мы реализовать try как шаблонную функцию?
О: Реализация try
требует возможности выходить из функции, содержащей вызов try
. В отсутствие такого super return
-выражения, try
не может быть реализовано в Go
даже если бы в нем были шаблонные функции. try
также требует переменного списка параметров различного типа. Мы не приветствуем поддержку таких шаблонных функций.
В: Я не могу использовать try в моем коде, моя логика проверки ошибок не вписывается в нужный паттерн. Что мне делать?
О: try
не предназначен для покрытия всех случаев обработки ошибок; он спроектирован для аккуратной обработки основных случаев, для сохранения простоты и ясности. Если рефакторинг вашего кода с использованием try
не имеет смысл (или невозможен), просто оставьте всё как есть. В конце концов, if
тоже конструкция языка.
В: В моей функции, большинство проверок на ошибки требует разной обработки. Я могу использовать try, но при использовании defer всё становится совсем сложно. Что мне делать?
О: Вы можете разделить вашу функцию на несколько маленьких функций, каждая из которых имеет общую обработку ошибок. Ну и смотрите предыдущий вопрос.
В: Чем try
отличается от обработки исключений (и где catch
)?
О: try
— синтаксический сахар ("макрос") для выдергивания полезных значений из выражений, возвращающих ошибку, с последующим выходом по условию (если была обнаружена ненулевая ошибка) из объемлющей функции. try
всегдя используется явно; он должен буквально присутствовать в исходном коде. Его эффект на поток исполнения ограничен текущей функцией. Также нет никакого механизма "поймать" ошибку. После того как функция вышла, выполнение на стороне вызывающего кода продолжается как обычно. В общем, try
— это сокращенная форма возврата по условию. С другой стороны, обработка исключений, которая в некоторых языках включает выражения throw
и try-catch
сродни обработке паники в Go. Исключение, которое может быть явно брошено или неявно возбуждено (например, ошибка деления на ноль), прерывает текущую активную функцию (путем возврата из нее) и продолжает разворачивать стек вызовов, прерывая вызывающую функцию и так далее. Исключение может быть "поймано" если оно возникло в блоке try-catch
, и дальше этой точки оно не распространяется. Исключение, которое не было поймано, может вызвать прекращение работы всей программы. В Go эквивалентом исключений является паника. Выброс исключений эквивалентен вызову panic
, а обработка исключений эквивалентна восстановлению из паники.
Комментарии (12)
AlexLeonov
24.10.2019 00:09— Давайте откажемся от исключений!
— Да-да, исключения зло, использование их для управления потоком исполнения надо карать страшными карами!
— Фигня выходит, давайте добавим try чтобы не писать кучу тупого кода на каждый вызов каждой функции?
— Да, давайте…gudvinr
24.10.2019 02:05— Давайте не будем вводить исключения, чтобы не усложнять язык
— Да, если писать идиоматичный код, то он выходит линейным и понятным любому человеку (вспомним ещё, что гугл использует C++ без исключений, к примеру), поэтому нет никакого смысла включать их в дизайн
— Вот есть много людей, которые не привыкли к языкам без встроенного механизма исключений, к тому же Go эволюционировал и теперь на нём пишут много различных программ. Может быть, подумаем над механизмом, который бы позволил помочь людям по-прежнему писать понятный код, но сделал его более компактным?
— Да, хорошая идея.
— Постойте! Может быть, поязвим над тем какие глупые авторы языка, который создавался с определенными целями и в определенных условиях, и надо было исключения добавить как у всех?
P.S. Я не говорю что этот proposal хороший. Но код на Go можно писать читаемым и без исключений, если не тащить с собой характерные принципы другого языка и говорить что это язык плохой. Это в принципе относится к любому языку, в прочем.
Не надо судить о таких вещах основываясь на личном опыте. Не нравится что нет исключений — ну не пишите на Go, возьмите свой любимый язык где они есть. Или попробуйте вникнуть в особенности нового языка и писать на нём, а не делать кальку с языковых конструкций других.
0xd34df00d
24.10.2019 01:58где expr означает выражение входного параметра (обычно вызов функции), возвращающее n+1 значений типов T1, T2,… Tn и error для последнего значения. Если expr является одинарным значением (n=0), это значение должно быть типом error и try не возвращает результат. Вызов try с выражением, которое не возвращает последнее значение типа error ведет к ошибке компиляции.
Вот интересно, монады я понимаю, зависимые типы понимаю, а здесь ничего понять не смог с трёх прочтений.
TonyLorencio
24.10.2019 09:09Самое хорошее в этом proposal — что его все-таки закрыли.
Самое плохое — что такое поведение поощряло бы проброс ошибок наверх (привет исключениям) вместо их надлежащей обработки (см. Errors are values).
PsyHaSTe
24.10.2019 14:21Угу, обожаю, когда функция решает что она самая умная и вместо того чтобы просто вернуть ошибку пытается вернуть нулл, упасть с паникой или еще что-нибудь оригинальное сделать.
Принцип самурая — то, как нужно жить.
rustler2000
Уже ж закопали. Или премодерация заняла три месяца?
tumbler Автор
Ну это не повод не набросить :)
TheShock
Что ж, подыграю вам.
Каждый очередной подобный «proposal» — в очередной раз подсвечивает ущербность этого языка. Я вот читал статью и меня постоянно немного тошнило от вещей, которые автор описывает с восторгом.
makarychev13
В любом случае интересно наблюдать за всей этой историей с обработкой ошибок в go. Многие просто смеются над гошникамии и говорят, что они никак не могут осилить обычный try/catch, но и этот подход хоть и самый популярный, но не общепризнанный. В haskell, например, не особо любят эксепшены и предпочитают всякие MayBe и Either. Интересно, чем это закончится. Может, Пайк придумает нечто совсем революционное.
mayorovp
В Хаскеле как бы есть языковые средства, которые делают Either таким же удобным как исключения...
makarychev13
Никто и не спорит. Мне вообще обработка ошибок в хаскеле больше нравится, чем обычные try/catch.