Pull to refresh
-14
darkit@darkitread⁠-⁠only

User

Send message
В go практически любая функция может вернуть ошибку

Начнем с того что ошибка в го ничем не отличается от значения. И их очень легко пропустить и пойти дальше с ошибочным значением.
res, err := makeSomething() мы забили на ошибку и пошли дальше с результатом res — ни в джаве, ни в расте такого уродства вы не получите


Аналогичный код на java писать практически невозможно:

на джаве вы можете писать


  • используя checked exceptions
  • unchecked exceptions
  • взять vavr и гонять везде Either
  • сделать такую же фигню как и в Го и возвращать Pair

и не забываем что в Го вы так же можете получить Panic

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

вот смотрите пример чистой логики на джава, которая читает сообщение из кафки и потом в нее же записывает.


    @Incoming("fruit-in")
    @Outgoing("fruit-out")
    fun makeSomething(item: Fruit): Fruit {
        ...clean logic
    }

можно теперь пример кода на Го, который затмит сие творение?


А у меня код на go выглядит чисто и просто так просто потому, что он на go.

опять же можно пример такого чистого кода?


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


Flux.just(url1, url2, ...)
    .flatMap(url -> getContent(url).onErrorReturn("default value"), 5)
    .reduce("", (accum, content) -> accum + content)

а теперь попробуйте вы показать такое же на Го, а потом мы начнет усложнять задачу — прикрутить ретрай, разные варианты реагирования на разные ошибки, кеширование и тд.


скрывает сотни других однотипных конструкций в java

чесно, я вообще не понимаю какие конструкции вы имеете ввиду.

Скорость разработки выше для относительно простых задач

что в вашем понимании относительно простая задача?

На go можно писать вообще без IDE

А зачем?


где без автоматической генерации сотен конструкций

У меня прикручен ломбок и объекты выглядят чисто и просто.


а условный jetbrains с go — это плохо?

Для меня Го не плохо, а шаг назад. Я должен писать тонны однотипного кода, вместо сосредоточения на бизнес логике.

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

нет, потому что хоть го и будет меньше памяти наверное есть по сравнению с монстрообразным спрингбутом, но скорость разработки отобьет все расходы на память.

Пока вы будете писать


res, err := makeSomething()
if(err != nil) {
    ....
}
...

Проекты на спрингбуте или кваркусе уже будут в продакшене :)))

Но я же говорю про библиотеки вообще.

Мне кажется что здесь и ошибка. Библиотеки не живут в вакууме, за редким исключением. И мы в данном случае говорим про обработку ошибок в ФП стиле, значит что моя библиотека будет отдавать ошибку в этой парадигме и делать свою реализацию для Try|Either вообще нет смысла.

А почему бы и нет? Например в resilience4j оно используется и ничем не мешает.

Поэтому мне нравится Reactor(или RxJava) — что там есть Mono/Flux и через них удобно выразить и обработать и ошибки (Mono.error()) и пустые значения ('Mono.empty()') и асинхронность с коробки.

Мне нужно выйти из метода без ошибки, если вернулся пустой Mono.
Буду благодарен, если расскажете как это сделать без Optional

Вместо такого кода


return Mono.zip(
  transactionRepository.findByUniqueKey(transactionKey)
    .map(Optional::of)
    .defaultIfEmpty(Optional.empty()),
  accountRepository.findById(fromAccountId)
    .switchIfEmpty(Mono.error(new AccountNotFound())),
  accountRepository.findById(toAccountId)
    .switchIfEmpty(Mono.error(new AccountNotFound())),
).flatMap(withMDC(fetched -> {
  var foundTransaction = fetched.getT1();
  var fromAccount = fetched.getT2();
  var toAccount = fetched.getT3();
  if (foundTransaction.isPresent()) {
    log.warn("retry of transaction " + transactionKey);
    return Mono.empty();
  }
  ...
}

я бы сделал отдельный обьект для хранения информации:


@Value
@Builder
public class TransactionOperation {
    Account from;
    Account to;
    Transaction  transaction;
}

и тогда код стал бы


Mono.just(TransactionOperation.builder())
                .flatMap(builder -> Flux.merge(
                        transactionRepository.findByUniqueKey(transactionKey)
                                .doOnNext(builder::transaction),
                        accountRepository.findById(fromAccountId)
                                .doOnNext(builder::from),
                        accountRepository.findById(toAccountId)
                                .doOnNext(builder::to)
                ).last().map(ignored -> builder.build()))
                .filter(this::validate)
                .map(ops -> {...});
...
    private boolean validate(@NonNull TransactionOperation ops) {
        if (null == ops.getFrom()) {
            throw new AccountNotFound();
        }
        if (null == ops.getTo()) {
            throw new AccountNotFound();
        }
        if (null != ops.getTransaction()) {
            log.warn("retry of transaction " + transactionKey);
            return false;
        }
        return true;
    }

Буквально на этой неделе нашёл у себя багу, что не все ошибки из метода логировались, как >раз потому что не сразу видно на что навешен обработчик.

я обычно ставлю в самом конце цепочти doOnError(err -> log.error("Unexpected exception", err))

Stacktrace не показывает каким образом мы попали в проблемное место.

Очень помагает ReactorDebugAgent.init(); Все становится красивым и понятным.


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

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


Вот такой подход он неверный, тк он не реактивный.


transactionRepository.findByUniqueKey(transactionKey)
    .map(Optional::of)
    .defaultIfEmpty(Optional.empty())

зачем вы возвращаете монаду Optional<Something> чтобы завернуть ее потом в Mono<Optional<Something>> — если у вас уже реактивный стек так почему не так


public Mono<Something> findByUniqueKey(...) {...}

Многоступенчатая вложенность кода из-за flatMap.

Опять же — бейте на слои, классы и функции чтобы все было плоским, как то так .flatMap(this::createRequest) а уже вложеная логика идет в createRequest и в результате получается понятно и плоско.


Неудобная обработка ошибок и их выброс.

Наоборот, оборачивание повсюду try...catch забирает кучу сил, а так есть разные методы как реагировать на ошибку и на каком уровне.


Сложная обработка поведения для Mono.empty().

Ничуть не сложнее чем Optional.empty + заставляет вас убирать null тем самым код становится менее бажный.


Сложности с логированием, если надо в лог добавить что-то глобальное, например traceId. (в >статье не описываю, но те же проблемы с другими ThreadLocal переменными, например >SpringSecurity)

Достаточно один раз написать свой паблишер, который добавить в reactor.core.publisher.Hooks#onEachOperator(Function) и он будет пробрасывать в контекст все что вам нужно и не захламлять бизнес код работой с MDC.


Неудобно дебажить.

Опять же, проблема в ваших простынях. Такой код и на Java Streams будет тяжело понимать и дебажить.


Неявное api для параллелизации.

Переводом на Реактор я наоборот значительно сократил код который работал со многими потоками. Все что надо есть из коробки, как для работы реактивных библиотек типа Lettuce так и для старого кода когда надо плодить потоки.

Как же тогда масштабироваться, если на входе много данных?

бейте на много партишинов ваш топик

и да и нет.
у вас активных консюмеров топика не может быть больше чем кол-ва партишинов. но вы можете держать про запас у вас могут быть разные топики и следовательно кафка распределит консюмеров по ним.

И что происходит, если данных в разделе/теме много и они не помещаются в локальную базу?

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

Kafka Streams может умереть и по другой причине: не пойманные исключения в вашем коде.… Второй вариант кажется лучше — совсем не ловить исключение и дать Kafka Streams умереть...

Вот это меня удивляет больше всего — почему они не могут просто откатывать оффсет и репроцесить сообщение, если добавлять счетчик ретраем в метаданные то было бы вообще хорошо. И тогда пользовательский код решал что бы хватит уже ретраить или же продолжаем ретраить пока не подымется база.


А так стримы хороши когда сами в себе :(

а там их несколько и надо смотреть на свободный.

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

а была бы супруга то таскала бы вас по музеям и стали бы отличать Моне от Мане :)))

когда человека все устраивает то он не пишет плаксивые статьи на техническом сайте

Information

Rating
Does not participate
Registered
Activity