Там обозначена проблема, а за решением - ищите пропозалы.
Только там синтаксический сахар .... Не исключения!
Лол. Вы сначала этот сахар нарекаете "исключениями", а потом говорите с такой экспрессией, будто я предлагал сделать настоящие исключения в Go. Не надо придумывать мне позицию)
Я напоминаю, что речь изначально была о том недостатке, что эти хэндлеры можно забыть удалить. Вы вынуждены можете пробежаться по цепочке вызовов в поисках подходящих return.
Только он становится еще более многословным и неудобным, чем Go
Это все слова, выше я приводил практически идентичный код, где вся разница - в наличии скобок вокруг кортежа и new перед ошибкой.
можно же еще боксинг-анбоксинг накрутить
Без него в Java не обойтись. В Go при оборачивании ошибки будет боксинг исходной ошибки, к слову.
var banTime = TimeSpan::FromMinutes(5);
try {
var user = users.GetUser(currentUserId);
try {
if (badUsersCache.TryGet(user.Id, out var record) && record.After(DateTime.Now)) {
return new Response(Status.BadRequest, $"you are to comment after {record.BlockedTill}");
}
}
catch (CacheException ex) {
_logger.Log("comment blocker unavailable");
}
if (user.IsBadGuy()) {
blockCommentsFor(user, DateTime.Now.Add(banTime));
}
return new Response(Status.OK);
}
catch (ErrUserUnknown ex) {
return new Response(Status.NotAuthorized, "user unknown");
}
catch (ErrSessionExpired ex) {
return new Response(Status.NotAuthorized, "session expored");
}
catch (Exception ex) {
return new Response(Status.BadRequest, "something went wrong");
}
Или еще лучше
var banTime = TimeSpan::FromMinutes(5);
try {
var user = users.GetUser(currentUserId);
if (!TryCheckCanComment(user.Id, out var badRequestResponse)) {
return badRequestResponse;
}
if (user.IsBadGuy()) {
blockCommentsFor(user, DateTime.Now.Add(banTime));
}
return new Response(Status.OK);
}
catch (ErrUserUnknown ex) {
return new Response(Status.NotAuthorized, "user unknown");
}
catch (ErrSessionExpired ex) {
return new Response(Status.NotAuthorized, "session expored");
}
catch (Exception ex) {
return new Response(Status.BadRequest, "something went wrong");
}
bool TryCheckCanComment(long userId, out Response response) {
response = default;
try {
if (badUsersCache.TryGet(user.Id, out var record) && record.After(DateTime.Now)) {
response = new Response(Status.BadRequest, $"you are to comment after {record.BlockedTill}");
return false;
}
}
catch (CacheException ex) {
_logger.Log("comment blocker unavailable");
}
return true;
}
...за счет того, что:
обработка ошибок уезжает ниже;
блок try{...} задает границы, выделяющие зависимые части алгоритма.
В итоге код бизнес-логики читается гораздо легче:
var user = users.GetUser(currentUserId);
if (!TryCheckCanComment(user.Id, out var badRequestResponse)) {
return badRequestResponse;
}
if (user.IsBadGuy()) {
blockCommentsFor(user, DateTime.Now.Add(banTime));
}
return new Response(Status.OK);
это же как раз тот случай, когда ошибку надо обработать на месте.
И тут же приводите пример, где для GetUser обрабатываются разные типы ошибок.
привычный образ для меня
То есть семантически то же самое, только синтаксисом другого языка. Однако, цель здесь - демонстрация того, что условный C#, несмотря на наличие исключений, при обработке ошибок может запросто "деградировать" до подхода Go там, где нужна наглядность обработки ошибок и раскрутка стека вызовов не нужна. (Впрочем, это может и Java, и большинство прочих языков с исключениями, если отказаться от кортежей, которые при наличии дженериков могут быть запросто заменены на обобщенную структуру-пару)
гошный сервер сииильно меньше. Он умеет те самые, в лучшем случае, 10% от того, что умеет NGINX, которые нужны приложению.
Если это важно для конкретного случая, то можно поискать сервер меньшего размера.
Кеширование, маршрутизация - это часть логики веб-приложения. Та самая часть, которая у вас находится снаружи, а у гоферов - прямо в коде.
Можно сделать и внутри PHP-приложения, аналогично Go.
Проблема с запросом - я смотрю в лог приложения, php-кодеры - в лог приложения и в лог контейнера.
Все логи будут в логах контейнера, иначе сделать - нужно еще постараться.
В Go из рантайм-зависимостей - только glibc.
Ну вот будет у вас патченная glibc. Если вы решили побалоываться альтернативными образами для PHP, почему бы и здесь не упороться?)
Ошибки совместимости, внезапно, ловит. Вот прям буквально не соберется.
Выяснять то, что вы подразумеваете под ошибками совместимости мне лениво) Бизнес-логика могла поменяться, при сохранении контрактов - все будет собираться, но работать ожидаемым образом не будет.
Вопрос в том, как догадаться, нужно ли этот самый ? писать.
Зачем догадываться, если можно знать наверняка? Почитать доку, навести курсор на функцию. Если функция возвращает Result<>, то это значит, что она может возвращать ошибку такого-то типа. Если программист вдруг ошибется - компилятор напомнит.
разве ? не признак того, что функция может вернуть null?
В Rust нет null, а с Option<T> этот оператор не работает. Если Option<T>::None нужно сконвертировать в ошибку, то необходимо сделать это явно.
(Сейчас с дженериками это должно элегантнее выглядеть, чем эти костыли с кодогенеторами)
Отлично - это слишком громко сказано) По сути, это оборачивание потенциального нулла паттерном ошибки, чтобы пользователя принудить проверить значение несоответствием типов. То есть в Go, в терминах Rust, нет Option, есть лишь корявая реализация Result. Почему корявая? Невалидная переменная значения даже в случае такой "ошибки" будет доступна. Типы-суммы в Rust не позволяют получить невалидное значение, ибо enum-переменная может иметь лишь одно значение: либо результат, либо ошибка. Мухи от котлет будут отделены на этапе компиляции.
Да, можно сказать, что в здравом уме в Go никто не будет использовать это значение, но гарантии от компилятора - все равно прикольнее, чем сырая надежда на здравый смысл)
На месте, или в конце блока?
На месте или выше по стеку вызовов.
Ровно то же самое же в Go можно сделать.
Было бы странно, если было бы нельзя) Вы же пытались проблему с повисающим невалидным значение результата выдать за полезный "результат ошибки".
Поэтому в Go и рекомендуется ошибки обрабатывать в месте возникновения.
Но не всегда это возможно и разумно. В частности - в публичном API, под капотом которого может быть куча логики с разными ошибками, которые пользователю может быть полезно отличать.
Так, например?
В целом - идея такая. Псевдокод подозрительно напоминает C#, удивляет, что вы не использовали кортежи привычным образом)
var (res, err) = FooCanFail(142);
if (err != null) {
// ...
}
(int result, Error err) FooCanFail(int input)
=> input > 10
? (42, null)
: (default, new Error($"Argument {nameof(input)} is out of range"));
Ради такой штуки от кортежей же отказаться придется
А зачем они? При желании, кортеж все так же может быть альтернативным (успешным) значением результата, например - Result<(u8, String, MyType), std::io::Error>
Своя прелесть в наличии контракта о возможной ошибке в виде сигнатуры метода перед глазами, имхо, все-таки есть.
Оператор ? после вызова - это уже верный признак того, что функция может возвращать ошибку. Либо где-то рядом не двузначно происходит обработка ошибки.
publishRestirction = restrictions.Get(uderID) if publishRestriction != null {... Такая штука в логику не ляжет
Эта штука (?) нужна только для проброса ошибки выше. Если нужно обработать на месте, то это и надо делать. Но касаемо примера, опциональные (нуллабельные) значения в Rust выражаются через отдельный enum - Option<T>, который может оборачивать значение Some(value) или же быть пустым - None. Эквивалент вашему примеру выглядел бы так:
if let Some(publish_restriction) = restrictions.get(user_id) {
//... existing publish_restriction usage
}
Если метод скаляр возвращает, оно же не сработает, цепочку рвать будем
Ничего не надо рвать, цепочка foo()?.bar().baz()? будет работать, с учетом, что функция bar никогда не возвращает ошибок (что можно понять по отсутствию ? после вызова). Оператор ? разворачивает значение так, что следующий за ним код ни сном, ни духом ни про какие ошибки, он "видит" только значение по успешному сценарию.
Если nil - результат ошибки, рядом с ним ошибка возвращаться должна
В каком смысле "результат ошибки"? Операция обычно либо удалась, либо нет. Если у ошибки есть какие-то свои вспомогательные данные, пусть даже опциональные, то их можно банально положить внутрь структуры самой ошибки.
Сильно длиннее ?. И чем длиннее цепочка вызовов, тем веселее: foo()?.bar()?.baz()?
В смысле, полноту кортежа? Полноту по всем видам/типам/экземплярам возвращаемых ошибок, к сожалению, не проверит.
Вот про это.
Если еще иерархия ошибок поддерживается (типа чтобы PackageError -> ReadError -> [NotFoundError,DuplicateError] позволяло ReadError проверить, и наследников проигнорить)
Никакие иерархии "из коробки" не поддерживаются - как напишите, так и будет)
В вашем примере в соседней ветке io::Error и num::ParseIntError оборачиваются в CliError - благодаря реализациям трейта From для CliError, которые неявно вызываются оператором ? если он видит несоответствие типу возврата open_and_parse_file. В процессе обработки ошибку разворачиваете и обрабатываете так, как посчитаете нужным.
Благо, всю эту машинерию руками писать не нужно, в языке есть макросы и сообщество напилило уже всяких инструментовавтоматизации.
enum CliError {
IoError(io::Error),
ParseError(num::ParseIntError),
}
// ...
fn main() {
match open_and_parse_file("some file") {
Ok(_value) => todo!(),
Err(err) => match err {
CliError::IoError(_io_err) => todo!(),
// non-exhaustive patterns: `CliError::ParseError(_)` not covered
// CliError::ParseError(_parse_err) => todo!(),
// no variant or associated item named `SomeExtraError`
// found for enum `CliError` in the current scope
CliError::SomeExtraError(_extErr) => todo!(),
}
}
}
Здесь, благодаря enum и match, будут ошибки компиляции в случаях:
если не обработан какой-то тип возможной ошибки (CliError::ParseError),
если из CliError удалили ожидаемую ошибку, а обработчик остался висеть (CliError::SomeExtraError).
Получится ли в Go эти ошибки поймать с его if-Is? Наверняка только второй случай и то без учета скоупа - если перенесли в другого "родителя", то ошибки не будет.
А Ok(num) - это если никакой ошибки не возникло же? но это же буквально if err != nil {} ?
Да-да, суть вашей претензии я в очередной раз понимаю и соглашаюсь.
Но если добавить уровень абстракции, унеся обработчики ошибок выше - в вызывающую функцию, то модель обработки ошибок с кастингом типов ошибок/Is Go садится в ту же лужу. Я об этом.
по коду не вижу, в каком месте может что-то пойти не так
Идея исключений в том, что не так не должно идти, на то они и называются исключениями. Ожидаемые ошибки должны обрабатываться явно и другими механизмами. Собственно, по этой причине сначала придрался до вас в примере со стором.
В Go такого разделения нет, поэтому ошибки валидации перемешиваются с разного рода критичными проблемами, вроде проблем сетевого взаимодействия, которые к бизнес-логике прямого отношения не имеют и обработка которых в большинстве случаев ограничивается логгированием и отображением юзеру сообщения, вроде: "Упс, что-то пошло не так! Повторите позже" или падения процесса насмерть. Читающему, желающему понять бизнес-логику, этот код не нужен, он мешает.
меня в ответ ссаными тряпками гоняют, мол, это уже давно не круто и вообще моветон...
Никто вас не гоняет, но так уж сложилось, что концепция checked exceptions оказалась недостаточно гибкой и больше вредит, чем помогает на практике. Я верю, что со временем рынок созреет к необходимости повышать качество софта и какой-то компромисс найдет отражение в синтаксисе.
А пока - будем писать тесты, внимательно читать документацию и стараться не ломать обратную совместимость, выбрасывая новые исключения там, где их не ждут.
Это не решение проблемы, это вынужденный отказ от удобства.
Кортеж с ошибкой на C#
Там обозначена проблема, а за решением - ищите пропозалы.
Лол. Вы сначала этот сахар нарекаете "исключениями", а потом говорите с такой экспрессией, будто я предлагал сделать настоящие исключения в Go. Не надо придумывать мне позицию)
Я напоминаю, что речь изначально была о том недостатке, что эти хэндлеры можно забыть удалить. Вы
вынужденыможете пробежаться по цепочке вызовов в поисках подходящихreturn
.Это все слова, выше я приводил практически идентичный код, где вся разница - в наличии скобок вокруг кортежа и
new
перед ошибкой.Без него в Java не обойтись. В Go при оборачивании ошибки будет боксинг исходной ошибки, к слову.
Сообщество говорит, что надо.
Будет выглядеть лучше
Или еще лучше
...за счет того, что:
обработка ошибок уезжает ниже;
блок
try{...}
задает границы, выделяющие зависимые части алгоритма.В итоге код бизнес-логики читается гораздо легче:
И тут же приводите пример, где для
GetUser
обрабатываются разные типы ошибок.То есть семантически то же самое, только синтаксисом другого языка.
Однако, цель здесь - демонстрация того, что условный C#, несмотря на наличие исключений, при обработке ошибок может запросто "деградировать" до подхода Go там, где нужна наглядность обработки ошибок и раскрутка стека вызовов не нужна. (Впрочем, это может и Java, и большинство прочих языков с исключениями, если отказаться от кортежей, которые при наличии дженериков могут быть запросто заменены на обобщенную структуру-пару)
Вы таки хотите сказать, что в Go нет функций, способных вызывать другие функции?
Не покажу. Асинхронщину же на PHP делают, предполагаю, что должны быть средства и стейт иметь.
Логи в контейнере как-то запрещают подключать метрики?
Это должно помешать ее пропатчить? Или компилятор Go.
Да, но это не повод выдавать некоторые риски за нестерпимую боль.
Опенсорс вам никаких гарантий не дает. Если вы берете левый билд, то на выходе можете получить что его автору угодно.
Конечно) Один раз выучил - радуешься все остальное время.
Если это важно для конкретного случая, то можно поискать сервер меньшего размера.
Можно сделать и внутри PHP-приложения, аналогично Go.
Все логи будут в логах контейнера, иначе сделать - нужно еще постараться.
Ну вот будет у вас патченная glibc. Если вы решили побалоываться альтернативными образами для PHP, почему бы и здесь не упороться?)
Выяснять то, что вы подразумеваете под ошибками совместимости мне лениво) Бизнес-логика могла поменяться, при сохранении контрактов - все будет собираться, но работать ожидаемым образом не будет.
@qrKot
Я обманул, с
Option<T>
try-оператор тоже работает (пример бестолковый, ради самого факта).Зачем догадываться, если можно знать наверняка?
Почитать доку, навести курсор на функцию. Если функция возвращает
Result<>
, то это значит, что она может возвращать ошибку такого-то типа. Если программист вдруг ошибется - компилятор напомнит.В Rust нет
null
, а сOption<T>
этот оператор не работает. ЕслиOption<T>::None
нужно сконвертировать в ошибку, то необходимо сделать это явно.(Сейчас с дженериками это должно элегантнее выглядеть, чем эти костыли с кодогенеторами)
Отлично - это слишком громко сказано) По сути, это оборачивание потенциального нулла паттерном ошибки, чтобы пользователя принудить проверить значение несоответствием типов. То есть в Go, в терминах Rust, нет
Option
, есть лишь корявая реализацияResult
. Почему корявая? Невалидная переменная значения даже в случае такой "ошибки" будет доступна. Типы-суммы в Rust не позволяют получить невалидное значение, ибо enum-переменная может иметь лишь одно значение: либо результат, либо ошибка. Мухи от котлет будут отделены на этапе компиляции.Да, можно сказать, что в здравом уме в Go никто не будет использовать это значение, но гарантии от компилятора - все равно прикольнее, чем сырая надежда на здравый смысл)
На месте или выше по стеку вызовов.
Было бы странно, если было бы нельзя) Вы же пытались проблему с повисающим невалидным значение результата выдать за полезный "результат ошибки".
Но не всегда это возможно и разумно. В частности - в публичном API, под капотом которого может быть куча логики с разными ошибками, которые пользователю может быть полезно отличать.
В целом - идея такая. Псевдокод подозрительно напоминает C#, удивляет, что вы не использовали кортежи привычным образом)
Как напишите, так и будет. Но обычно ошибки шарятся на модуль, в котором может быть больше одной публичной функции.
Обязательство писать отдельные обработчики по каждому элементу списка с себя можно снять через discard pattern (
_
).Я ж обещал, что
match
- это ваша мечта про checked exceptions)А зачем они? При желании, кортеж все так же может быть альтернативным (успешным) значением результата, например -
Result<(u8, String, MyType), std::io::Error>
Оператор
?
после вызова - это уже верный признак того, что функция может возвращать ошибку. Либо где-то рядом не двузначно происходит обработка ошибки.Эта штука (
?
) нужна только для проброса ошибки выше. Если нужно обработать на месте, то это и надо делать. Но касаемо примера, опциональные (нуллабельные) значения в Rust выражаются через отдельный enum -Option<T>
, который может оборачивать значениеSome(value)
или же быть пустым -None
. Эквивалент вашему примеру выглядел бы так:Ничего не надо рвать, цепочка
foo()?.bar().baz()?
будет работать, с учетом, что функцияbar
никогда не возвращает ошибок (что можно понять по отсутствию?
после вызова). Оператор?
разворачивает значение так, что следующий за ним код ни сном, ни духом ни про какие ошибки, он "видит" только значение по успешному сценарию.В каком смысле "результат ошибки"? Операция обычно либо удалась, либо нет. Если у ошибки есть какие-то свои вспомогательные данные, пусть даже опциональные, то их можно банально положить внутрь структуры самой ошибки.
Если мы знаем, что
err
- этоnil
, почему бы не написать явно?Сильно длиннее
?
. И чем длиннее цепочка вызовов, тем веселее:foo()?.bar()?.baz()?
Вот про это.
Никакие иерархии "из коробки" не поддерживаются - как напишите, так и будет)
В вашем примере в соседней ветке
io::Error
иnum::ParseIntError
оборачиваются вCliError
- благодаря реализациям трейтаFrom
дляCliError
, которые неявно вызываются оператором?
если он видит несоответствие типу возвратаopen_and_parse_file
. В процессе обработки ошибку разворачиваете и обрабатываете так, как посчитаете нужным.Благо, всю эту машинерию руками писать не нужно, в языке есть макросы и сообщество напилило уже всяких инструментов автоматизации.
Это же не эквивалентный код. Для Rust код с пробрасываем ошибок выше, а для Go вы уже снаружи - делаете обработку.
Дополню ваш пример вызовом
open_and_parse_file
:Здесь, благодаря
enum
иmatch
, будут ошибки компиляции в случаях:если не обработан какой-то тип возможной ошибки (
CliError::ParseError
),если из
CliError
удалили ожидаемую ошибку, а обработчик остался висеть (CliError::SomeExtraError
).Получится ли в Go эти ошибки поймать с его if-Is? Наверняка только второй случай и то без учета скоупа - если перенесли в другого "родителя", то ошибки не будет.
Это буквально
return num, nil
Да-да, суть вашей претензии я в очередной раз понимаю и соглашаюсь.
Но если добавить уровень абстракции, унеся обработчики ошибок выше - в вызывающую функцию, то модель обработки ошибок с кастингом типов ошибок/
Is
Go садится в ту же лужу. Я об этом.Идея исключений в том, что не так не должно идти, на то они и называются исключениями. Ожидаемые ошибки должны обрабатываться явно и другими механизмами. Собственно, по этой причине сначала придрался до вас в примере со стором.
В Go такого разделения нет, поэтому ошибки валидации перемешиваются с разного рода критичными проблемами, вроде проблем сетевого взаимодействия, которые к бизнес-логике прямого отношения не имеют и обработка которых в большинстве случаев ограничивается логгированием и отображением юзеру сообщения, вроде: "Упс, что-то пошло не так! Повторите позже" или падения процесса насмерть. Читающему, желающему понять бизнес-логику, этот код не нужен, он мешает.
Никто вас не гоняет, но так уж сложилось, что концепция checked exceptions оказалась недостаточно гибкой и больше вредит, чем помогает на практике. Я верю, что со временем рынок созреет к необходимости повышать качество софта и какой-то компромисс найдет отражение в синтаксисе.
А пока - будем писать тесты, внимательно читать документацию и стараться не ломать обратную совместимость, выбрасывая новые исключения там, где их не ждут.
Почитать две статьи и выставить нужные флаги при сборке - великий труд, действительно)
Про все вместе.
Что ж вы тогда портянку-сравнение на Go не написали в этот раз?
Можно заменить на
Is
, только обозначенную проблему это не исправит.