
Программисты на Go уже давно и долго жалуются на слишком многословную обработку ошибок. Все мы близко (а иногда и болезненно) знакомы со следующим шаблоном кода:
x, err := call()
if err != nil {
// обработка err
}
Проверка if err != nil
встречается настолько часто, что может становиться объёмнее остального кода. Обычно это происходит в программах, выполняющих много вызовов API, в которых обработка ошибок рудиментарна и они просто возвращаются. Некоторые программы в итоге выглядят примерно так:
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err
}
y, err := strconv.Atoi(b)
if err != nil {
return err
}
fmt.Println("result:", x + y)
return nil
}
Из десятка строк кода тела этой функции реальную работу выполняют только четыре (вызовы и последние две строки). Остальные шесть строк — это шум. Код слишком многословен, поэтому неудивительно, что жалобы на обработку ошибок уже годами находятся на вершине списков в опросах разработчиков. (Какое-то время жалобы на обработку ошибок обгоняла только досада из-за отсутствия дженериков, но теперь, когда Go поддерживает дженерики, обработка ошибок снова вернулась на первое место).
Команда разработчиков Go воспринимает отзывы сообщества со всей серьёзностью, поэтому мы много лет пытались придумать решение этой проблемы.
Первая явная попытка, предпринятая командой Go, произошла ещё в 2018 году, когда Расс Кокс формально описал проблему в рамках того, что мы тогда называли инициативой Go 2. Он изложил возможное решение, основанное на дизайне драфта Марселя ван Лохузена. Дизайн был основан на механизме check
и handle
и казался достаточно целостным. В драфт был включён подробный анализ альтернативных решений, в том числе сравнения с методиками, используемыми в других языках. Если у вас возникнет вопрос, рассматривалась ли раньше какая-то конкретная методика обработки ошибок, то прочитайте этот документ!
// реализация printSum, использующая предложенный механизм check/handle.
func printSum(a, b string) error {
handle err { return err }
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
Решение с check
и handle
расценили слишком сложным, и почти год спустя, в 2019 году, мы представили гораздо более простое и печально известное сегодня предложение try
. Оно развивало идеи check
и handle
, но псевдоключевое слово check
превратилось во встроенную функцию try
, а от блока handle
мы избавились. Чтобы изучить эффект встроенной функции try
, мы написали простой инструмент (tryhard), переписывающий обработку ошибок так, чтобы она использовала try
. Предложение активно обсуждали: оно набрало девятьсот комментариев в GitHub issue.
// реализация printSum с использованием предложенного механизма try.
func printSum(a, b string) error {
// используем defer, чтобы повысить удобство ошибок перед возвратом
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}
Однако try
влияла на поток управления, поскольку в случае ошибки выполняла возврат из внешней функции из выражений с потенциально глубокой вложенностью, что скрывало бы этот поток управления. Из-за этого наше предложение оказалось неприемлемым для многих разработчиков, и, несмотря на большие усилия, приложенные к разработке этого предложения, мы решили отказаться и от него. Оглядываясь назад, можно предположить, что лучше было бы ввести новое ключевое слово: это можно сделать сегодня, потому что теперь у нас есть гораздо более удобный контроль за версией языка благодаря файлам go.mod
и директивам, применяемым к конкретным файлам. Ограничив использование try
только присвоениями и операторами, мы бы, возможно, могли решить часть других проблем. Недавнее предложение Джимми Фрэша, которое, по сути, возвращается к исходному дизайну check
и handle
, устраняя некоторые его недостатки, движется в этом направлении.
Предложение try
вызвало обширную реакцию и обсуждения, в том числе привело к публикации серии постов Расса Кокса «Thinking about the Go Proposal Process». В ней он пришёл к выводу, что мы снизили шансы на более удачный результат, представив почти полностью готовое предложение, почти не оставив места для отзывов сообщества и установив «пугающие» сроки внедрения. Цитата из «Go Proposal Process: Large Changes»: «сегодня можно сказать, что try
было достаточно большим изменением […] которое должно было стать дизайном второго драфта, а не окончательным предложением с графиком внедрения». Но вне зависимости от возможных неудач в реализации процесса и коммуникаций, пользователи в целом были сильно против этого предложения.
В то время у нас не было более приемлемого решения, и мы много лет не занимались разработкой изменений синтаксиса обработки ошибок. Однако многих участников сообщества вдохновил наш пример, и образовался стабильный поток предложений с решениями проблемы обработки ошибок. Многие из них были очень похожи друг на друга, часть была интересной, часть непонятной, часть нереализуемой. Чтобы отслеживать расширяющуюся территорию изменений, ещё спустя год Иэн Лэнс Тейлор создал зонтичный issue со сводкой текущего состояния предложений по улучшению обработки ошибок. Для упорядочивания отзывов, обсуждений и статей по теме была создана Go Wiki. Независимо от нас другие люди начали собирать коллекции множества предложений по обработке ошибок, поступающих в течение всех этих лет. Их общий объём впечатляет: например, попробуйте изучить пост Шона Ляо «go error handling proposals».
Жалобы на отсутствие краткости при обработке ошибок продолжали поступать (см. Go Developer Survey 2024 H1 Results), поэтому после серии постепенно совершенствовавшихся внутренних предложений разработчиков Go Иэн Лэнс Тейлор опубликовал в 2024 году пост «reduce error handling boilerplate using ?
». На этот раз идея заключалась в заимствовании конструкции, реализованной в Rust, а именно оператора ?
. Расчёт был на то, что, положившись на уже готовый механизм с устоявшимся форматом и учтя наши уроки за много лет, мы наконец-то сможем добиться прогресса. В небольших неформальных исследованиях пользователей, в которых программистам показывали код на Go с использованием ?
, подавляющее большинство участников правильно разобралось со смыслом кода, и это убедило нас сделать ещё одну попытку. Чтобы иметь возможность оценить влияние изменений, Иэн написал инструмент, преобразующий обычный код на Go в код, использующий новый синтаксис предложения, а также добавил прототип этой фичи в компилятор.
// реализация printSum с использованием предложенного оператора "?".
func printSum(a, b string) error {
x := strconv.Atoi(a) ?
y := strconv.Atoi(b) ?
fmt.Println("result:", x + y)
return nil
}
К сожалению, как и в случае с другими идеями по обработке ошибок, новое предложение тоже быстро получило множество комментариев и рекомендаций по незначительным изменениям, часто основанных на личных предпочтениях. Иэн закрыл предложение и перенёс его содержимое в обсуждение, чтобы упростить общение и собрать дополнительные отзывы. Немного видоизменённая версия была воспринята чуть позитивнее, но в целом всё равно не получила широкой поддержки.
Спустя столько лет безуспешных попыток, накопив три полномасштабных предложения со стороны команды разработчиков Go и в буквальном смысле сотни (!) предложений сообщества, большинство из которых оказалось вариациями на одну и ту же тему, ни одно из них не завоевало достаточной (не говоря уже о подавляющей) поддержки. Возник вопрос: как нам двигаться дальше? И нужно ли двигаться вообще?
Мы думаем, что нет.
Точнее, мы считаем, что нужно перестать пытаться решить синтаксическую проблему; по крайней мере, в ближайшем будущем. Процесс согласования предложений оправдывает это решение следующим образом:
Цель процесса согласования предложений заключается в обеспечении общего консенсуса о результате в приемлемых временных рамках. Если при проверке предложений не удаётся прийти к общему консенсусу в обсуждении issue в issue tracker, то обычно результатом становится отклонение предложения.
Кроме того:
Может оказаться так, что при проверке предложения общего консенсуса добиться не удаётся, но тем не менее очевидно, что предложение не должно быть полностью отклонено. […] Если группа проверки предложения не может ни прийти к консенсусу, ни к следующему этапу предложения, то решение о дальнейшем пути возлагается на архитекторов Go […], которые изучают обсуждение и стремятся прийти к консенсусу в своём кругу.
Ни одному из предложений по обработке ошибок не удалось достичь ничего похожего на консенсус, поэтому все они были отклонены. Даже самые опытные члены команды Go в Google не смогли на текущий момент единогласно выбрать наилучший путь (возможно, ситуация когда-нибудь изменится). Но без подавляющего консенсуса мы не можем двигаться вперёд.
Вот весомые аргументы в пользу сохранения статуса-кво:
Если бы в Go синтаксический сахар обработки ошибок возник на ранних этапах развития, то немногие бы оспаривали его сегодня. Но прошло уже 15 лет, возможность упущена; к тому же, в Go есть вполне удобный способ обработки ошибок, пусть он иногда и кажется слишком длинным.
Рассмотрим это под другим углом: допустим, мы пришли сегодня к идеальному решению. Если внедрить его в язык, то мы получим вместо одной недовольной группы пользователей (голосующих за изменения) другую (предпочитающую статус-кво). Мы находились в схожей ситуации, когда решили добавить в язык дженерики, однако здесь есть важное отличие: сегодня никого не заставляют использовать дженерики, а хорошие дженерик-библиотеки написаны так, что пользователи благодаря выводу типов по большей части могут закрыть глаза на то, что это дженерики. Если же в язык добавят новую синтаксическую конструкцию для обработки ошибок, то практически всем придётся использовать её, чтобы поддерживать идиоматичность кода.
Предотвращение появления дополнительного синтаксиса соответствует одному из правил дизайна Go: не создавать нескольких способов реализации одной и той же функциональности. Из этого правила есть исключения в активно используемых областях, например, в присвоениях. Забавно, что возможность повторного объявления переменных в коротких объявлениях переменных (
:=
) была добавлена для решения проблемы, возникшей из-за обработки ошибок: без повторных объявлений цепочки проверок на ошибки требуют переменнойerr
с разным для каждой проверки именем (или добавления объявлений отдельных переменных). В то время, вероятно, лучше было бы добавить больше синтаксической поддержки обработки ошибок. Тогда правило повторного объявления могло и не понадобиться, а благодаря его отсутствию и не возникли бы различные связанные с ним осложнения.-
Если изучить код обработки ошибок, то можно заметить, что многословность становится не так важна, если ошибки действительно обрабатываются. Для хорошей обработки ошибок часто требуется добавление к ошибке дополнительной информации. Например, в опросах пользователи часто оставляют комментарии о нехватке связанных с ошибками трассировок стеков. Эту проблему можно решить при помощи вспомогательных функций, создающих и возвращающих расширенную ошибку. В этом примере (гипотетическом) относительный объём бойлерплейта намного меньше:
func printSum(a, b string) error { x, err := strconv.Atoi(a) if err != nil { return fmt.Errorf("invalid integer: %q", a) } y, err := strconv.Atoi(b) if err != nil { return fmt.Errorf("invalid integer: %q", b) } fmt.Println("result:", x + y) return nil }
-
Новая функциональность стандартной библиотеки также может снизить объём бойлерплейта обработки ошибок в духе поста Роба Пайка «Errors are values». Например, в некоторых случаях для одновременной обработки серии ошибок можно использовать
cmp.Or
:func printSum(a, b string) error { x, err1 := strconv.Atoi(a) y, err2 := strconv.Atoi(b) if err := cmp.Or(err1, err2); err != nil { return err } fmt.Println("result:", x+y) return nil }
Написание, чтение и отладка кода — это достаточно различающиеся действия. Написание повторяющихся проверок на ошибки может казаться скучным занятием, но современные IDE имеют мощные функции автозавершения кода, некоторые даже с поддержкой LLM. Таким инструментам очень легко писать базовые проверки на ошибки. Многословность при чтении кода по большей части проста в понимании, но и в этом могут помочь инструменты. Например, IDE с настройками языка Go может иметь переключатель для сокрытия кода обработки ошибок. Такие переключатели уже существуют для других блоков кода, например, для тел функций.
При отладке кода обработки ошибок полезна возможность быстрого добавления
println
или отдельной строки для установки контрольной точки в отладчике. Это легко реализовать, если уже существует отдельный операторif
. Но если вся логика обработки ошибок скрыта заcheck
,try
или?
, то может потребоваться преобразование всего кода в обычную конструкциюif
, что усложняет отладку и может даже привести к появлению незаметных багов.Есть и соображения практичности: придумать новую идею синтаксиса для обработки ошибок легко, отсюда и берётся множество предпочтений со стороны сообщества разработчиков. Сложнее придумать хорошее решение, которое выдержало бы тщательный анализ. Для правильного проектирования изменений в языке и для их реализации требуются согласованные усилия. Но позже всё равно придётся платить за это высокую цену: весь код нужно будет переписать, обновить документацию и внести изменения в инструменты. С учётом всего этого, изменения в языке — очень дорогостоящий процесс, а команда разработчиков Go относительно мала и у неё есть много других приоритетных задач. (Но здесь всё может поменяться: приоритеты становятся другими, размеры команд уменьшаются и увеличиваются.)
Кроме того, некоторые из нас недавно получили возможность присутствовать на Google Cloud Next 2025, где у команды Go был свой павильон и где мы провели небольшой Go Meetup. Все пользователи Go, с которыми нам удалось поговорить, уверенно заявили, что нам не следует менять язык ради улучшения обработки ошибок. Многие сообщали, что отсутствие поддержки обработки ошибок в Go наиболее очевидна, когда переходишь на него с другого языка, где такая поддержка есть. Когда больше осваиваешься и начинаешь писать более идиоматичный код на Go, эта проблема становится гораздо менее важной. Разумеется, выборка была недостаточно репрезентативной, но множество людей на GitHub и их отзывы тоже можно считать ещё одним примером данных.
Разумеется, в пользу изменений тоже есть весомые аргументы:
Отсутствие качественной поддержки обработки ошибок остаётся основной причиной жалоб в опросах пользователей. Если команда Go действительно воспринимает отзывы пользователей серьёзно, то рано или поздно нам придётся с этим что-то делать. (Хотя подавляющей поддержки изменений в языке всё равно не наблюдается).
Наверно, неправильно было бы стремиться исключительно к снижению количества символов. Было бы лучше сделать стандартную обработку ошибок хорошо заметной при помощи ключевого слова, и при этом всё равно избавиться от бойлерплейта (
err != nil
). Такой подход может упростить чтение кода (для ревьюера!) и понимание того, какая ошибка обрабатывается, без необходимости дополнительной проверки, что повысило бы качество и безопасность кода. Это бы привело нас к самому началу, то есть кcheck
иhandle
.Мы не знаем, насколько на самом деле проблема синтаксической многословности проверки ошибок важнее, чем многословность хорошей обработки ошибок: конструирование ошибок — полезная часть API, важная и для разработчиков, и для конечных пользователей. Этот аспект нам бы хотелось изучить глубже.
Итак, пока ни одна попытка решения проблемы обработки ошибок не получила достаточной популярности. Если взглянуть на текущую ситуацию объективно, то нужно признать, что у нас нет ни всеобщего понимания проблемы, ни согласия о том, существует ли вообще эта проблема. Учитывая всё это, мы приняли следующее прагматичное решение:
В обозримом будущем команда разработчиков Go перестанет пытаться внедрить в язык синтаксические изменения для обработки ошибок. Также мы без подробного изучения будем закрывать все открытые и новые предложения, основной смысл которых связан с синтаксисом обработки ошибок.
Сообщество разработчиков приложило огромные усилия к исследованию и обсуждению этих вопросов. Пусть они и не привели к каким-то осязаемым изменениям в синтаксисе обработки ошибок, этот труд позволил нам улучшить многие другие аспекты языка Go и процессов. Возможно, в будущем у нас возникнет более чёткая картина относительно обработки ошибок. А пока мы собираемся сосредоточиться на развитии новых возможностей, улучшающих Go для всех и каждого.
Комментарии (54)
rsashka
04.06.2025 11:56В обозримом будущем команда разработчиков Go перестанет пытаться внедрить в язык синтаксические изменения для обработки ошибок. Также мы без подробного изучения будем закрывать все открытые и новые предложения, основной смысл которых связан с синтаксисом обработки ошибок.
А просто сделать классические исключения не позволяет сделать религия языка?
Dhwtj
04.06.2025 11:56это плохо
хотя бы тем что
создает неявный поток управления
большие затраты на разматывание стека
rsashka
04.06.2025 11:56Конечно плохо, но не этим (поток управления в catch явный, а затраты не такие уж и большие по сравнению с чистотой кода и элементарной логикой обработки ошибок с помощью исключений).
Плохо тем, что если исключения в языке есть, то ими можно не пользоваться (из-за якобы неявного потока управления или "больших затрат"), но если их нет, то приходится писать потрянки кода или и вовсе ошибки игнорировать.
Dhwtj
04.06.2025 11:56Не явный
Ты никак не узнаешь, кидает функция исключения или нет. Если кидает то она не чистая и хрен что получится при использовании pipe
let a = b.c().d()
Tishka17
04.06.2025 11:56Ну допустим мы придумали простой способ помечать, что она кидает исключение. Проблема решена? Или все таки вы настаиваете что для пробросе ошибки наверх (что происходит чаще чем обработка) обязательно нужен бойлерплейт
Dhwtj
04.06.2025 11:56Ответил чуть раньше. Пожалуй, основная проблема что исключение прервёт обработку остальной части коллекции
Quintanar
04.06.2025 11:56А явные ошибки чем лучше? Точно так же любая функция может вернуть непонятно что, непонятно при каких условиях. Мы с легкостью можем свести исключения к ошибкам - везде добавить catch, а обратное не получится.
NeoCode
04.06.2025 11:56А существующий механизм panic-recover это разве не оно?
Slach
04.06.2025 11:56нет вообще не оно... хотя и выглядит походим местами
https://www.perplexity.ai/search/pochemu-panic-i-recover-v-gola-6lMaybMLSNeZvPJ2_cNm2Q
yrub
04.06.2025 11:56да-да, конечно плохо, еще дженерики это тоже было плохо и много чего другого, чего нет в go))
поток управления на 100% явный
затраты есть, но на то это и называется "исключение", они не предназначены для того, чтобы вы кидали их миллионами и просто так. они нужны для того, чтобы сломать поток выполнения, когда вы начинаете выполнять код с ложной предпосылки
насчет чистоты: c этим есть споры, все же исключение это про control flow а не про состояние. то что вы написали это не pipe а цепочка вызовов, все понятно что будет и в стектрейсе все будет написано. Даже в java там где реальный пайп и лямбды (я про стримы) - все равно все понятно. Не понятно только когда у вас асинхронно и в разных потоках вызывается, но и на этот случай есть решение через инструментирование кода, но это уже мы о несколько другом.
Мораль басни проста: надо интересоваться тем, что есть за пределами вашего стека и относиться критически к любым "это не баг, а фича" или "зато все явно и понятно".
Dhwtj
04.06.2025 11:56Плохо хотя бы тем, что исключение внутри pipe для коллекции или потока остановит обработку всей коллекции, а если функциональная обработка ошибок, то не будет обработан только сбойный элемент
Мораль басни проста: надо интересоваться тем, что есть за пределами вашего стека и относиться критически к любым "это не баг, а фича" или "зато все явно и понятно".
Может, вам и начать?
var numbers = lines // IEnumerable<string> .Select(int.Parse) // FormatException → итерация рвётся .Select(n => n * 2); // уже не выполнится
IEnumerable<int> ParseSafe(IEnumerable<string> src) { foreach (var s in src) try { yield return int.Parse(s) * 2; } catch (FormatException e) { Log.Warn($"skip {s}: {e.Message}"); } // 4 служебные строки на шаг }
Вместо этого уродства можно писать
var good = lines .Select(s => Try(int.Parse, s)) // string -> Result<int> .Select(r => r.Map(n => n * 2)) // Result<int> .Partition(); // (IEnumerable<int> ok, IEnumerable<Ex> err)
yrub
04.06.2025 11:56вы продолжаете думать об исключении как об ошибке чего-то. а это именно _ исключение_, это значит, что вам скорее всего надо прервать выполнение вашего пайпа в принципе. Думайте об этом как о транзакции. Очевидно, что иногда лучше возвращать ошибку вместо исключения, я это и так знаю и не спорю, я ж говорю, что хорошо в языке иметь концепцию исключений и применять ее по адресу, а не говорить что это все от сатаны, что оно не нужно, что оно не понятно, что оно плохо и все такое (хотя скорее всего причина их не добавления была технического рода)
bogolt
04.06.2025 11:56затраты есть, но на то это и называется "исключение", они не предназначены для того, чтобы вы кидали их миллионами и просто так.
Ну так а обычные ошибки как раз предназначены чтобы их кидали, и кидали при необходимости много. Тот же strconv может кинуть ошибку, и если моя программа выбирает числа из тонны мусора то и ошибки там будут сыпаться как из рога изобилия.
johngetman
04.06.2025 11:56добавляем резулты из раста, и получаем литерали лучший язык на текущий момент
AuToMaton
04.06.2025 11:56Программисты на Go уже давно и долго жалуются на слишком многословную обработку ошибок.
Вот тут следовало бы посмотреть - какие именно программисты. Все подряд? Начинающие, которые постепенно перестают? Опытные, которые доходят до сложных проектов и тогда начинают жаловаться? Это три большие разницы…
Если бы в Go синтаксический сахар обработки ошибок возник на ранних этапах развития, то немногие бы оспаривали его сегодня. Но прошло уже 15 лет, возможность упущена; к тому же, в Go есть вполне удобный способ обработки ошибок, пусть он иногда и кажется слишком длинным.
Интересно, какой такой сахар. Вся статья доказывает - при принятых принципах построения языка его быть не может. Реверанс в пользу кого-то, он же политес?
Все пользователи Go, с которыми нам удалось поговорить, уверенно заявили, что нам не следует менять язык ради улучшения обработки ошибок.
Это похоже на критику устройств Эппл - их больше всего критикуют те, у кого их нет. И что ответственные товарищи верят не всему из того, что вопят безответственные, это замечательно.
Если изучить код обработки ошибок, то можно заметить, что многословность становится не так важна, если ошибки действительно обрабатываются.
Если не этим ограничиться, то на этом можно было остановиться. Как по мне - вопрос снят.
Моё личное впечатление - Go подталкивает к тому, чтобы писать функции, в которых не может возникнуть ошибок. Типа ой,
strconv.Atoi(a)
может не сработать, спасите помогите ошибок обрабатывать. А что это за a? Может это пользователь ввёл? Тогда надо было на всякий случай супротив кульных хацкеров санитарный контроль, а то и вовремя вежливо попросить передумать.А может, если не сработало, то может можно дефолт или предыдущее значение взять. А может нужно проверить - а не прописью ли число…
В обозримом будущем команда разработчиков Go перестанет пытаться внедрить в язык синтаксические изменения для обработки ошибок. Также мы без подробного изучения будем закрывать все открытые и новые предложения, основной смысл которых связан с синтаксисом обработки ошибок.
Это замечательное отношение к работе. Не ожидал, что в тени Гугол и при выраженной популярности языка такое возможно. Было бы так с Julia или Scheme или Lua - ну допустим, но с Go - это респект.
КоролеваЯ в восхищении.Что интересно, оригинал - пост в блоге, почему-то именуемый статьёй, без возможности оставить комментарий. Так что моё желание своё восхищение выразить - увы.
Единственно что я могу сказать за try, так это то, что его возможности больше чем у механизма обработки ошибок в Go. С try удобно обрабатывать исчерпание стека или памяти, сигнал со стороны ОС, может быть арифметические проблемы, команду на останов горутины в конце концов.
slonopotamus
04.06.2025 11:56Это похоже на критику устройств Эппл - их больше всего критикуют те, у кого их нет.
Дык может у них потому и нет устройств Эппл.
С другой стороны есть когнитивное искажение, если мозг в прошлом принял какое-то решение (например, купить устройство Эппл), он будет очень стараться защитить и не пересматривать это решение.
Dhwtj
04.06.2025 11:56С try удобно обрабатывать исчерпание стека или памяти, сигнал со стороны ОС, может быть арифметические проблемы, команду на останов горутины в конце концов
Удобно, да. Но это небольшой набор случаев. Да и ничего страшного в функциональном стиле обработки таких ошибок.
VanKrock
04.06.2025 11:56В vlang сделали так:
fn print_sum(a string, b string) ! { x := strconv.atoi(a) or { return err } y := strconv.atoi(b) or { return err } println('result: ${x+y}') } fn main() { print_sum('123', '456') or { println(err)} }
В принципе удобно
Octagon77
04.06.2025 11:56Может не "сделали", а "делают", согласно правилам русского языка применительно к бете?
Дальше страницы сайта vlang не смотрел, так что спрошу - компилятор даёт возможность or опустить? Если нет - в принципе пойдёт.
Но я бы со своими вкусами предпочёл, чтобы Go такое забраковал - хорошо когда or это or и ничего больше. И хорошо когда язык не навязывает понятие ошибки, в Go ошибка - просто значение, решение как делать было (типа) принято не языком, а авторами функций которые могут возвращать ошибку, так гибче. И как тут быть в стиле Олега Тинькова - сомнительно, но ОК? А в Go - такое элементарно.
VanKrock
04.06.2025 11:56"делают" это если бы они сказали будет так, мы уже делаем, а тут именно это уже готово. То что бета - означает, что есть ошибки и некоторые конструкции могут измениться в будущем. Конструкцию or опустить нельзя (на самом деле вроде если постараться то можно, например если переменная может принять ошибку как валидный результат например !int, но это тоже нужно указать явно, после вызова метода поставить !) or в vlang это обработка ошибки и ничего более, это не
||
ну а то, что ошибка имеет стандартный интерфейс - это тоже хорошо, никто не мешает возвращать несколько значений как в gofn get() (int, int) { return 1, 2 }
Dhwtj
04.06.2025 11:56многословно
тогда уж
fn print_sum(a string, b string) ! { x := strconv.atoi(a) or err y := strconv.atoi(b) or err println('result: ${x+y}') } fn main() { print_sum('123', '456') or { println(err)} }
если последняя переменная по умолчанию возвращается как в раст
VanKrock
04.06.2025 11:56если написать
x := strconv.atoi(a) or {err}
то он будет пытаться присвоить x := err и компилятор выдаст ошибку так как нельзя присвоить IError в int
зато можно писать такx := strconv.atoi(a) or { 0 }
Dhwtj
04.06.2025 11:56Надо возвращать result < T, Err>
Как в нормальных языках
VanKrock
04.06.2025 11:56Можно кстати вообще так
fn print_sum(a string, b string) ! { x := strconv.atoi(a)! y := strconv.atoi(b)! println('result: ${x+y}') } fn main() { print_sum('a', '456') or { println(err)} }
Dhwtj
04.06.2025 11:56Функция без указания типа?
Подразумевается void?
А если задуматься, то и не void совсем, а ХЗ что.
Не, я в такие игры не играю
Result<(), Error>
) видимоЛучше это отображать явно
Cfyz
04.06.2025 11:56В интернетах периодически можно видеть обсуждения а не добавить ли в C механизм defer? И там тоже половина воспринимает идею в штыки, мол не надо в наш простой и понятный C тащить всякие неявные defer, есть же goto.
И вот представляется мне как разработчики на Go смотрят на это и думают, вот чудные, такой простой, понятный и удобный механизм не хотят использовать. А потом переключаются на соседнюю вкладку и критикуют синтаксический сахар обработки ошибкок, мол не надо в наш простой и понятный Go тащить всякие неявные try, есть же if =).
На это смотрят разработчики например на Rust и думают, вот чудные, такой простой, понятный и удобный механизм не хотят использовать. А потом переключаются на соседнюю вкладку и...
Sulerad
04.06.2025 11:56Мне кажется, тут есть большое отличие в подходах к языку. Если брать Си и Го, то в их дизайне очень большое влияние имеет простота и минимализм, а любой сахар и новые фичи воспринимаются если не в штыки, то с большим сомнением.
Если же брать раст или, скажем, плюсы, то это сложные языки, которые спокойно позволяют себе иметь больше десяти страниц документации. И соответственно они обычно не отбрасывают решения для реально болящих у людей проблем с мотивацией «не надо этого нам тащить никогда». Что, впрочем, не отменяет возможности для RFC застрять в состоянии «идея хорошая, но непонятно как это красиво впихнуть, запланировали подумать лет через пять».
Как пример — в го несколько разных хороших и проработанных вариантов обработки ошибок оказались отброшены в основном потому что в комьюнити не нашли всеобщей поддержки. А вот в том же расте есть let-else statement, в обсуждении которого под 200 комментариев и который полностью совпадает по функционалу с имеющейся конструкцией match или if, но имеет более
упоротыйузкоспециализированный синтаксис. И ничего, пободались, приняли, довели до stable.
bogolt
04.06.2025 11:56x, err1 := strconv.Atoi(a) y, err2 := strconv.Atoi(b) if err := cmp.Or(err1, err2); err != nil { return err }
вот этот пример максимально нердужелюбен к код ревью. Потому в дифе будет например\
x, err1 := strconv.Atoi(a) y, err2 := strconv.Atoi(b)
и читающему изменения в коде покажется что первая ошибка не обрабатывается, и чтобы проверить так ли это придётся лезть дальше в код. В общем предложение выглядит максимально странно и мне кажется лишь породит больше скрытых проблем.
checkpoint
04.06.2025 11:56А можно я попробую. Чисто дедовскими методами, без всяких try/catch и прочих непонятных операторов, без скрытых переменных и лишних скобок захламляющих код. Почти Python. ;)
#include <stdio.h> #include <stdlib.h> #include <string.h> extern int errno; int main(int argc, char *argv[]) { long a, b, c; if(argc < 4) return 255, printf("Usage: %s <long> <long> <long>\n", argv[0]); a = strtol(argv[1], NULL, 0); b = strtol(argv[2], NULL, 0); c = strtol(argv[3], NULL, 0); if(errno) return errno, printf("Error: %s\n", strerror(errno)); printf("Result: %ld\n", a + b + c); return 0; } rz@butterfly:~ % cc -Wno-unused-value a.c rz@butterfly:~ % ./a.out 1 2 Usage: ./a.out <long> <long> <long> rz@butterfly:~ % ./a.out 1 2 3 Result: 6 rz@butterfly:~ % ./a.out 1 2 -4 Result: -1 rz@butterfly:~ % ./a.out 1 2 0xffff Result: 65538 rz@butterfly:~ % ./a.out 1 2 fsck Error: Invalid argument rz@butterfly:~ % ./a.out fsck 2 3 Error: Invalid argument rz@butterfly:~ % ./a.out 1 fsck 0 Error: Invalid argument rz@butterfly:~ % ./a.out 1 3 444444444444444444444444444444444444444444444444 Error: Result too large
Panzerschrek
04.06.2025 11:56Очень плохой код. Забыть проверить значение errno - проще простого. Передать дополнительную информацию о причине ошибки тоже не возможно, есть только целочисленный код ощибки, в который подробностей не впихнуть.
checkpoint
04.06.2025 11:56Всё тоже самое можно отнести и к try/catch, и к описанному в статье решению.
Достоинство дедовского метода состоит в том, что он не требует никакого синтаксического сахара, а код локаничен и понятен. В таком включе можно писать на любом языке, хоть на макроассемблере. Изобретатели же новых языков находятся в постоянном поиске, они придумывают конструкции которые только усложняют жизнь.
есть только целочисленный код ощибки, в который подробностей не впихнуть.
А зачем Вам подробности ? В подавляющем случае достаточно просто знать была ошибка при выполнении какого-то промежуточного шага программы или нет. Код в errno вполне самодостаточен достаточен.
Panzerschrek
04.06.2025 11:56придумывают конструкции которые только усложняют жизнь
В Rust нельзя игнорировать Result типы - компилятор породит ошибку. А чтобы не игнорировать и просто пробросить ошибку, существует оператор
?
. Это отдельное синтаксическое нововведение, но оно упрощает жизнь, делая написание некорректного кода очень сложным и вынуждая писать правильно.Код в errno вполне самодостаточен достаточен.
Нет. Например, если файл не сумели открыть, полезно сохранить имя файла. Или если сетевое соединение не прошло, кроме кода что-то ещё может быть полезно. В Rust существует даже библиотека, которая предназначена для добавления контекста в ошибки.
klanev
04.06.2025 11:56Только при наличии потоков (и вообще параллельщины) errno - просто антипаттерн
checkpoint
04.06.2025 11:56Откуда Вы это взяли ? errno is thread safe очень давно. Никаких проблем с нитями у errno нет.
Panzerschrek
04.06.2025 11:56И тут приходит асинхронщина через корутины, и вдруг выясняется, что errno внезапно меняется, ибо корутина была перекинута планировщиком на другой поток, или же другая корутина в этом же потоке между делом успела поменять значение errno.
checkpoint
04.06.2025 11:56Кто мешает в yeild() сделать сохранение errno на ряду с другими параметрами контекста ? В общем-то это уже давно сделано за вас.
Dhwtj
04.06.2025 11:56почти идентично и с теми же недостатками
https://habr.com/ru/articles/915468/comments/#comment_28395806
Panzerschrek
04.06.2025 11:56Вся эта история - отличное подтверждение несостоятельности основополагающих принципов, на которых базируется язык Go. Несколько лет переливали из пустого в порожнее, пытались найти способ сделать лучше,соответствующий этим принципам, да ещё и так, чтобы все довольны были, но так в итоге ни к чему и не пришли. Программистам на Go всё так же предлагают писать бойлерплейт, чреватый ошибками.
По факту любое из присланных предложение было бы лучше, чем совсем ничего. Но в погоне за принципами решили остаться ни с чем, чем с чем-то.
WayMax
04.06.2025 11:56не создавать нескольких способов реализации одной и той же функциональности.
Замечательная задумка. А как на счет ее реализации? Например сгенерировать страничку с текстом "Hello World!"? Это же просто и наверняка существует только один способ сделать это?
func route1(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
}func route2(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello World!")
}func route3(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}Dhwtj
04.06.2025 11:56Это вопрос к библиотекам, не?
WayMax
04.06.2025 11:56"Не". Здесь не используются библиотеки. Только стандартные возможности языка.
olivera507224
04.06.2025 11:56То есть, вы планируете запустить ваш код без того чтобы написать после имени модуля следующие строки?
import ( "http" "io" "fmt" )
NeoCode
Из-за вот этого правила
никогда ни к чему и не придут. Потому что правило в целом неверное. Но и много разных несовместимых между собой способов и синтаксисов добавлять тоже не нужно, это будет еще хуже чем сейчас.
А нужно разработать универсальный способ обработки ошибок, включающий максимум различных возможностей и предложений, и разработать такой синтаксис, чтобы конкретные предложения оказались частными случаями этого максимально общего способа.
Dhwtj
но если нельзя создать несколько частных способов реализации то это не поможет
значит, правило вредное
NeoCode
В том то и дело, что несколько не связанных между собой частных не нужно, нужен один общий, но такой в котором все другие (в том числе и существующая явная обработка) окажутся именно частными случаями.
Например, что мне нравится в Go так это единый синтаксис для обычных функций и лямбд. Во всех других языках изощряются со всякими стрелками и прочими спецсимволами, а в Go и там и там func - просто для лямбд не указывается имя. То есть единый синтаксис и внутри два частных случая - именованные функции и анонимные.