Pull to refresh
1
0.1

.NET программист

Send message

хендлеры ошибки в Go принято писать сразу за той инструкцией, которая ее может выкинуть.

Это не решение проблемы, это вынужденный отказ от удобства.

Код аналогичный, только обернутый в try/catch?

Кортеж с ошибкой на C#

Не увидел там требований вкорячить исключения

Там обозначена проблема, а за решением - ищите пропозалы.

Только там синтаксический сахар .... Не исключения!

Лол. Вы сначала этот сахар нарекаете "исключениями", а потом говорите с такой экспрессией, будто я предлагал сделать настоящие исключения в 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, и большинство прочих языков с исключениями, если отказаться от кортежей, которые при наличии дженериков могут быть запросто заменены на обобщенную структуру-пару)

Выше по стеку вызовов может не быть. Мы же в Go.

Вы таки хотите сказать, что в Go нет функций, способных вызывать другие функции?

Пример кода inmemory-кеша в php-приложении покажете?

Не покажу. Асинхронщину же на PHP делают, предполагаю, что должны быть средства и стейт иметь.

Логов контейнера хватит всем! А мы зачем-то с какими-то метриками возимся, с трассировками, кастомными логгерами.

Логи в контейнере как-то запрещают подключать метрики?

Glibс - это API ядра Linux.

Это должно помешать ее пропатчить? Или компилятор Go.

Меньше зависимостей = меньше головной боли

Да, но это не повод выдавать некоторые риски за нестерпимую боль.

компилятор на пару с Торвальдсом дают определенные гарантии

Опенсорс вам никаких гарантий не дает. Если вы берете левый билд, то на выходе можете получить что его автору угодно.

Конечно) Один раз выучил - радуешься все остальное время.

гошный сервер сииильно меньше. Он умеет те самые, в лучшем случае, 10% от того, что умеет NGINX, которые нужны приложению.

Если это важно для конкретного случая, то можно поискать сервер меньшего размера.

Кеширование, маршрутизация - это часть логики веб-приложения. Та самая часть, которая у вас находится снаружи, а у гоферов - прямо в коде.

Можно сделать и внутри PHP-приложения, аналогично Go.

Проблема с запросом - я смотрю в лог приложения, php-кодеры - в лог приложения и в лог контейнера.

Все логи будут в логах контейнера, иначе сделать - нужно еще постараться.

В Go из рантайм-зависимостей - только glibc.

Ну вот будет у вас патченная glibc. Если вы решили побалоываться альтернативными образами для PHP, почему бы и здесь не упороться?)

Ошибки совместимости, внезапно, ловит. Вот прям буквально не соберется.

Выяснять то, что вы подразумеваете под ошибками совместимости мне лениво) Бизнес-логика могла поменяться, при сохранении контрактов - все будет собираться, но работать ожидаемым образом не будет.

@qrKot

разве ? не признак того, что функция может вернуть null?

Я обманул, с Option<T> try-оператор тоже работает (пример бестолковый, ради самого факта).

Вопрос в том, как догадаться, нужно ли этот самый ? писать.

Зачем догадываться, если можно знать наверняка?
Почитать доку, навести курсор на функцию. Если функция возвращает Result<>, то это значит, что она может возвращать ошибку такого-то типа. Если программист вдруг ошибется - компилятор напомнит.

разве ? не признак того, что функция может вернуть null?

В Rust нет null, а с Option<T> этот оператор не работает. Если Option<T>::None нужно сконвертировать в ошибку, то необходимо сделать это явно.

Optional и в Go отлично реализуем.
https://pkg.go.dev/github.com/markphelps/optional

(Сейчас с дженериками это должно элегантнее выглядеть, чем эти костыли с кодогенеторами)

Отлично - это слишком громко сказано) По сути, это оборачивание потенциального нулла паттерном ошибки, чтобы пользователя принудить проверить значение несоответствием типов. То есть в 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"));

Мы же этот списочек для каждого метода ведем?

Как напишите, так и будет. Но обычно ошибки шарятся на модуль, в котором может быть больше одной публичной функции.

у нас есть метод и список ошибок, которые он может вернуть. И мы должны (и даже обязаны) этот список обработать...

Обязательство писать отдельные обработчики по каждому элементу списка с себя можно снять через discard pattern (_).

Checked Exceptions, вроде, называется.

Я ж обещал, что match - это ваша мечта про checked exceptions)

Ради такой штуки от кортежей же отказаться придется

А зачем они? При желании, кортеж все так же может быть альтернативным (успешным) значением результата, например - 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 - результат ошибки, рядом с ним ошибка возвращаться должна

В каком смысле "результат ошибки"? Операция обычно либо удалась, либо нет. Если у ошибки есть какие-то свои вспомогательные данные, пусть даже опциональные, то их можно банально положить внутрь структуры самой ошибки.

Только return num, err оно, видимо.

Если мы знаем, что err - это nil, почему бы не написать явно?

не особенно длиннее matсh'а же

Сильно длиннее ?. И чем длиннее цепочка вызовов, тем веселее:
foo()?.bar()?.baz()?

В смысле, полноту кортежа?
Полноту по всем видам/типам/экземплярам возвращаемых ошибок, к сожалению, не проверит.

Вот про это.

Если еще иерархия ошибок поддерживается (типа чтобы PackageError -> ReadError -> [NotFoundError,DuplicateError] позволяло ReadError проверить, и наследников проигнорить)

Никакие иерархии "из коробки" не поддерживаются - как напишите, так и будет)

В вашем примере в соседней ветке io::Error и num::ParseIntError оборачиваются в CliError - благодаря реализациям трейта From для CliError, которые неявно вызываются оператором ? если он видит несоответствие типу возврата open_and_parse_file. В процессе обработки ошибку разворачиваете и обрабатываете так, как посчитаете нужным.

Благо, всю эту машинерию руками писать не нужно, в языке есть макросы и сообщество напилило уже всяких инструментов автоматизации.

Это же не эквивалентный код. Для Rust код с пробрасываем ошибок выше, а для Go вы уже снаружи - делаете обработку.

Дополню ваш пример вызовом 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 {} ?

Это буквально return num, nil

Да-да, суть вашей претензии я в очередной раз понимаю и соглашаюсь.

Но если добавить уровень абстракции, унеся обработчики ошибок выше - в вызывающую функцию, то модель обработки ошибок с кастингом типов ошибок/Is Go садится в ту же лужу. Я об этом.

по коду не вижу, в каком месте может что-то пойти не так

Идея исключений в том, что не так не должно идти, на то они и называются исключениями. Ожидаемые ошибки должны обрабатываться явно и другими механизмами. Собственно, по этой причине сначала придрался до вас в примере со стором.

В Go такого разделения нет, поэтому ошибки валидации перемешиваются с разного рода критичными проблемами, вроде проблем сетевого взаимодействия, которые к бизнес-логике прямого отношения не имеют и обработка которых в большинстве случаев ограничивается логгированием и отображением юзеру сообщения, вроде: "Упс, что-то пошло не так! Повторите позже" или падения процесса насмерть. Читающему, желающему понять бизнес-логику, этот код не нужен, он мешает.

меня в ответ ссаными тряпками гоняют, мол, это уже давно не круто и вообще моветон...

Никто вас не гоняет, но так уж сложилось, что концепция checked exceptions оказалась недостаточно гибкой и больше вредит, чем помогает на практике. Я верю, что со временем рынок созреет к необходимости повышать качество софта и какой-то компромисс найдет отражение в синтаксисе.

А пока - будем писать тесты, внимательно читать документацию и стараться не ломать обратную совместимость, выбрасывая новые исключения там, где их не ждут.

Почитать две статьи и выставить нужные флаги при сборке - великий труд, действительно)

Про все вместе.
Что ж вы тогда портянку-сравнение на Go не написали в этот раз?

Можно заменить на Is, только обозначенную проблему это не исправит.

Information

Rating
2,870-th
Registered
Activity

Specialization

Software Developer, Fullstack Developer
Senior
C#
Rust