Comments 59
Из-за вот этого правила
не создавать нескольких способов реализации одной и той же функциональности.
никогда ни к чему и не придут. Потому что правило в целом неверное. Но и много разных несовместимых между собой способов и синтаксисов добавлять тоже не нужно, это будет еще хуже чем сейчас.
А нужно разработать универсальный способ обработки ошибок, включающий максимум различных возможностей и предложений, и разработать такой синтаксис, чтобы конкретные предложения оказались частными случаями этого максимально общего способа.
но если нельзя создать несколько частных способов реализации то это не поможет
значит, правило вредное
В том то и дело, что несколько не связанных между собой частных не нужно, нужен один общий, но такой в котором все другие (в том числе и существующая явная обработка) окажутся именно частными случаями.
Например, что мне нравится в Go так это единый синтаксис для обычных функций и лямбд. Во всех других языках изощряются со всякими стрелками и прочими спецсимволами, а в Go и там и там func - просто для лямбд не указывается имя. То есть единый синтаксис и внутри два частных случая - именованные функции и анонимные.
В обозримом будущем команда разработчиков Go перестанет пытаться внедрить в язык синтаксические изменения для обработки ошибок. Также мы без подробного изучения будем закрывать все открытые и новые предложения, основной смысл которых связан с синтаксисом обработки ошибок.
А просто сделать классические исключения не позволяет сделать религия языка?
это плохо
хотя бы тем что
создает неявный поток управления
большие затраты на разматывание стека
Конечно плохо, но не этим (поток управления в catch явный, а затраты не такие уж и большие по сравнению с чистотой кода и элементарной логикой обработки ошибок с помощью исключений).
Плохо тем, что если исключения в языке есть, то ими можно не пользоваться (из-за якобы неявного потока управления или "больших затрат"), но если их нет, то приходится писать потрянки кода или и вовсе ошибки игнорировать.
Не явный
Ты никак не узнаешь, кидает функция исключения или нет. Если кидает то она не чистая и хрен что получится при использовании pipe
let a = b.c().d()
Ну допустим мы придумали простой способ помечать, что она кидает исключение. Проблема решена? Или все таки вы настаиваете что для пробросе ошибки наверх (что происходит чаще чем обработка) обязательно нужен бойлерплейт
А явные ошибки чем лучше? Точно так же любая функция может вернуть непонятно что, непонятно при каких условиях. Мы с легкостью можем свести исключения к ошибкам - везде добавить catch, а обратное не получится.
А существующий механизм panic-recover это разве не оно?
нет вообще не оно... хотя и выглядит походим местами
https://www.perplexity.ai/search/pochemu-panic-i-recover-v-gola-6lMaybMLSNeZvPJ2_cNm2Q
да-да, конечно плохо, еще дженерики это тоже было плохо и много чего другого, чего нет в go))
поток управления на 100% явный
затраты есть, но на то это и называется "исключение", они не предназначены для того, чтобы вы кидали их миллионами и просто так. они нужны для того, чтобы сломать поток выполнения, когда вы начинаете выполнять код с ложной предпосылки
насчет чистоты: c этим есть споры, все же исключение это про control flow а не про состояние. то что вы написали это не pipe а цепочка вызовов, все понятно что будет и в стектрейсе все будет написано. Даже в java там где реальный пайп и лямбды (я про стримы) - все равно все понятно. Не понятно только когда у вас асинхронно и в разных потоках вызывается, но и на этот случай есть решение через инструментирование кода, но это уже мы о несколько другом.
Мораль басни проста: надо интересоваться тем, что есть за пределами вашего стека и относиться критически к любым "это не баг, а фича" или "зато все явно и понятно".
Плохо хотя бы тем, что исключение внутри 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)
вы продолжаете думать об исключении как об ошибке чего-то. а это именно _ исключение_, это значит, что вам скорее всего надо прервать выполнение вашего пайпа в принципе. Думайте об этом как о транзакции. Очевидно, что иногда лучше возвращать ошибку вместо исключения, я это и так знаю и не спорю, я ж говорю, что хорошо в языке иметь концепцию исключений и применять ее по адресу, а не говорить что это все от сатаны, что оно не нужно, что оно не понятно, что оно плохо и все такое (хотя скорее всего причина их не добавления была технического рода)
затраты есть, но на то это и называется "исключение", они не предназначены для того, чтобы вы кидали их миллионами и просто так.
Ну так а обычные ошибки как раз предназначены чтобы их кидали, и кидали при необходимости много. Тот же strconv может кинуть ошибку, и если моя программа выбирает числа из тонны мусора то и ошибки там будут сыпаться как из рога изобилия.
добавляем резулты из раста, и получаем литерали лучший язык на текущий момент
хотели простоты языка?
страдайте теперь!
Давно не читал такого длинного "А не пошли бы вы все..."
Программисты на Go уже давно и долго жалуются на слишком многословную обработку ошибок.
Вот тут следовало бы посмотреть - какие именно программисты. Все подряд? Начинающие, которые постепенно перестают? Опытные, которые доходят до сложных проектов и тогда начинают жаловаться? Это три большие разницы…
Если бы в Go синтаксический сахар обработки ошибок возник на ранних этапах развития, то немногие бы оспаривали его сегодня. Но прошло уже 15 лет, возможность упущена; к тому же, в Go есть вполне удобный способ обработки ошибок, пусть он иногда и кажется слишком длинным.
Интересно, какой такой сахар. Вся статья доказывает - при принятых принципах построения языка его быть не может. Реверанс в пользу кого-то, он же политес?
Все пользователи Go, с которыми нам удалось поговорить, уверенно заявили, что нам не следует менять язык ради улучшения обработки ошибок.
Это похоже на критику устройств Эппл - их больше всего критикуют те, у кого их нет. И что ответственные товарищи верят не всему из того, что вопят безответственные, это замечательно.
Если изучить код обработки ошибок, то можно заметить, что многословность становится не так важна, если ошибки действительно обрабатываются.
Если не этим ограничиться, то на этом можно было остановиться. Как по мне - вопрос снят.
Моё личное впечатление - Go подталкивает к тому, чтобы писать функции, в которых не может возникнуть ошибок. Типа ой, strconv.Atoi(a)
может не сработать, спасите помогите ошибок обрабатывать. А что это за a? Может это пользователь ввёл? Тогда надо было на всякий случай супротив кульных хацкеров санитарный контроль, а то и вовремя вежливо попросить передумать.
А может, если не сработало, то может можно дефолт или предыдущее значение взять. А может нужно проверить - а не прописью ли число…
В обозримом будущем команда разработчиков Go перестанет пытаться внедрить в язык синтаксические изменения для обработки ошибок. Также мы без подробного изучения будем закрывать все открытые и новые предложения, основной смысл которых связан с синтаксисом обработки ошибок.
Это замечательное отношение к работе. Не ожидал, что в тени Гугол и при выраженной популярности языка такое возможно. Было бы так с Julia или Scheme или Lua - ну допустим, но с Go - это респект. Королева Я в восхищении.
Что интересно, оригинал - пост в блоге, почему-то именуемый статьёй, без возможности оставить комментарий. Так что моё желание своё восхищение выразить - увы.
Единственно что я могу сказать за try, так это то, что его возможности больше чем у механизма обработки ошибок в Go. С try удобно обрабатывать исчерпание стека или памяти, сигнал со стороны ОС, может быть арифметические проблемы, команду на останов горутины в конце концов.
Это похоже на критику устройств Эппл - их больше всего критикуют те, у кого их нет.
Дык может у них потому и нет устройств Эппл.
С другой стороны есть когнитивное искажение, если мозг в прошлом принял какое-то решение (например, купить устройство Эппл), он будет очень стараться защитить и не пересматривать это решение.
С try удобно обрабатывать исчерпание стека или памяти, сигнал со стороны ОС, может быть арифметические проблемы, команду на останов горутины в конце концов
Удобно, да. Но это небольшой набор случаев. Да и ничего страшного в функциональном стиле обработки таких ошибок.
"...или прошлое значение..."
Ну это прям "Счастливой отладки"!
В 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)}
}
В принципе удобно
Может не "сделали", а "делают", согласно правилам русского языка применительно к бете?
Дальше страницы сайта vlang не смотрел, так что спрошу - компилятор даёт возможность or опустить? Если нет - в принципе пойдёт.
Но я бы со своими вкусами предпочёл, чтобы Go такое забраковал - хорошо когда or это or и ничего больше. И хорошо когда язык не навязывает понятие ошибки, в Go ошибка - просто значение, решение как делать было (типа) принято не языком, а авторами функций которые могут возвращать ошибку, так гибче. И как тут быть в стиле Олега Тинькова - сомнительно, но ОК? А в Go - такое элементарно.
"делают" это если бы они сказали будет так, мы уже делаем, а тут именно это уже готово. То что бета - означает, что есть ошибки и некоторые конструкции могут измениться в будущем. Конструкцию or опустить нельзя (на самом деле вроде если постараться то можно, например если переменная может принять ошибку как валидный результат например !int, но это тоже нужно указать явно, после вызова метода поставить !) or в vlang это обработка ошибки и ничего более, это не ||
ну а то, что ошибка имеет стандартный интерфейс - это тоже хорошо, никто не мешает возвращать несколько значений как в go
fn get() (int, int) {
return 1, 2
}
многословно
тогда уж
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)}
}
если последняя переменная по умолчанию возвращается как в раст
если написать x := strconv.atoi(a) or {err}
то он будет пытаться присвоить x := err и компилятор выдаст ошибку так как нельзя присвоить IError в int
зато можно писать так x := strconv.atoi(a) or { 0 }
В интернетах периодически можно видеть обсуждения а не добавить ли в C механизм defer? И там тоже половина воспринимает идею в штыки, мол не надо в наш простой и понятный C тащить всякие неявные defer, есть же goto.
И вот представляется мне как разработчики на Go смотрят на это и думают, вот чудные, такой простой, понятный и удобный механизм не хотят использовать. А потом переключаются на соседнюю вкладку и критикуют синтаксический сахар обработки ошибкок, мол не надо в наш простой и понятный Go тащить всякие неявные try, есть же if =).
На это смотрят разработчики например на Rust и думают, вот чудные, такой простой, понятный и удобный механизм не хотят использовать. А потом переключаются на соседнюю вкладку и...
И критикуют Higher-kinded types
А может завидуют
Мне кажется, тут есть большое отличие в подходах к языку. Если брать Си и Го, то в их дизайне очень большое влияние имеет простота и минимализм, а любой сахар и новые фичи воспринимаются если не в штыки, то с большим сомнением.
Если же брать раст или, скажем, плюсы, то это сложные языки, которые спокойно позволяют себе иметь больше десяти страниц документации. И соответственно они обычно не отбрасывают решения для реально болящих у людей проблем с мотивацией «не надо этого нам тащить никогда». Что, впрочем, не отменяет возможности для RFC застрять в состоянии «идея хорошая, но непонятно как это красиво впихнуть, запланировали подумать лет через пять».
Как пример — в го несколько разных хороших и проработанных вариантов обработки ошибок оказались отброшены в основном потому что в комьюнити не нашли всеобщей поддержки. А вот в том же расте есть let-else statement, в обсуждении которого под 200 комментариев и который полностью совпадает по функционалу с имеющейся конструкцией match или if, но имеет более упоротый узкоспециализированный синтаксис. И ничего, пободались, приняли, довели до stable.
x, 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)
и читающему изменения в коде покажется что первая ошибка не обрабатывается, и чтобы проверить так ли это придётся лезть дальше в код. В общем предложение выглядит максимально странно и мне кажется лишь породит больше скрытых проблем.
А можно я попробую. Чисто дедовскими методами, без всяких 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
Очень плохой код. Забыть проверить значение errno - проще простого. Передать дополнительную информацию о причине ошибки тоже не возможно, есть только целочисленный код ощибки, в который подробностей не впихнуть.
Всё тоже самое можно отнести и к try/catch, и к описанному в статье решению.
Достоинство дедовского метода состоит в том, что он не требует никакого синтаксического сахара, а код локаничен и понятен. В таком включе можно писать на любом языке, хоть на макроассемблере. Изобретатели же новых языков находятся в постоянном поиске, они придумывают конструкции которые только усложняют жизнь.
есть только целочисленный код ощибки, в который подробностей не впихнуть.
А зачем Вам подробности ? В подавляющем случае достаточно просто знать была ошибка при выполнении какого-то промежуточного шага программы или нет. Код в errno вполне самодостаточен достаточен.
придумывают конструкции которые только усложняют жизнь
В Rust нельзя игнорировать Result типы - компилятор породит ошибку. А чтобы не игнорировать и просто пробросить ошибку, существует оператор ?
. Это отдельное синтаксическое нововведение, но оно упрощает жизнь, делая написание некорректного кода очень сложным и вынуждая писать правильно.
Код в errno вполне самодостаточен достаточен.
Нет. Например, если файл не сумели открыть, полезно сохранить имя файла. Или если сетевое соединение не прошло, кроме кода что-то ещё может быть полезно. В Rust существует даже библиотека, которая предназначена для добавления контекста в ошибки.
Только при наличии потоков (и вообще параллельщины) errno - просто антипаттерн
Откуда Вы это взяли ? errno is thread safe очень давно. Никаких проблем с нитями у errno нет.
И тут приходит асинхронщина через корутины, и вдруг выясняется, что errno внезапно меняется, ибо корутина была перекинута планировщиком на другой поток, или же другая корутина в этом же потоке между делом успела поменять значение errno.
почти идентично и с теми же недостатками
https://habr.com/ru/articles/915468/comments/#comment_28395806
Вся эта история - отличное подтверждение несостоятельности основополагающих принципов, на которых базируется язык Go. Несколько лет переливали из пустого в порожнее, пытались найти способ сделать лучше,соответствующий этим принципам, да ещё и так, чтобы все довольны были, но так в итоге ни к чему и не пришли. Программистам на Go всё так же предлагают писать бойлерплейт, чреватый ошибками.
По факту любое из присланных предложение было бы лучше, чем совсем ничего. Но в погоне за принципами решили остаться ни с чем, чем с чем-то.
не создавать нескольких способов реализации одной и той же функциональности.
Замечательная задумка. А как на счет ее реализации? Например сгенерировать страничку с текстом "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!")
}
Это вопрос к библиотекам, не?
"Не". Здесь не используются библиотеки. Только стандартные возможности языка.
То есть, вы планируете запустить ваш код без того чтобы написать после имени модуля следующие строки?
import (
"http"
"io"
"fmt"
)
Библиотек. Просто они позволяют получить одно и то же разными способами
А разве это одно и тоже? Совсем не одно и тоже. Или вы считаете, что если результат одинаков, то значит все одно и тоже? Может вам и целая плеяда fmt функций не подойдет? Что за идиотский пример? Мне бы было стыдно
Об (отсутствии) синтаксической поддержки обработки ошибок в Go