Как стать автором
Обновить

Proposal: try — встроенная функция проверки ошибок

Время на прочтение19 мин
Количество просмотров3K
Автор оригинала: Robert Griesemer

Краткое содержание


Предлагается новая конструкция 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, а обработка исключений эквивалентна восстановлению из паники.

Теги:
Хабы:
+6
Комментарии19

Публикации

Изменить настройки темы

Истории

Работа

Go разработчик
128 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн