Pull to refresh

Comments 89

Давно пора уже от этой слоеной архитектуры отказываться, горячо поддерживаю. Надоело уже это писать чтоб json в базу сохранить и обратно строчки из базы в виде json выдать. И так всё прекрасно понятно без этих репозиториев и прочих.

в приложении уровня туду лист для начинающих и правда так.

иногда логику все же надо писать, а иногда еще покрывать тестами

если у вас тупо круд приложение, то хоть сразу sql в хэндлере пишите)

суть статьи «когда много проектов- удобнее иметь общую структуру проекта»

Вот следующий шаг после "общей структуры" - а я сделал небольшое изменение/фикс в общий код, как теперь на все проекты распространить? И тут появляется необходимость в генераторе кода проекта с возможностью апдейта.

По всем слоям логику искать тоже не особо приятно, совсем не ускоряет процесс разработки.

Тестами и HTTP хендлеры прекрасно покрываются + в проекте бывает небольшой набор чисто вычислительных функций с хитрой логикой, для которых чистые юнит тесты отлично подходят.

Если что, то я отказываться от репозиториев не призываю, в 90% моих проектах есть этот слой. Если проект потенциально планируется большой, то на мой взгляд лучше перестраховаться и оставить репозитории. А для проектов поменьше вполне нормально обойтись и без него. Оба решения на моей личной практике не играют большой роли в скорости разработки и ни один подход не дает других значительных преимуществ. Просто немного удобнее когда код более компактный.

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

Если у вас простой CRUD, в котором самое сложное это валидация и сериализация, а приложению характерна жёсткая завязка на конкретную инфраструктуру - пишите все хоть в контроллере, это наиболее эффективный вариант. Внедрение CQS, аггрегатов, сервисов, репозиториев будет оверинжинирингом.

Для поиска ответа на эти вопросы, предлагаю Вам ознакомиться с гексагональной архитектурой и DDD - так будет куда понятнее, какую роль играют контроллеры, сервисы и причем тут репозиторий.

А мне наоборот трудно представить разработку условного веб-приложения без "портов" и "адаптеров". Адаптером может быть что угодно - локалсторадж, какие-нибудь эксперементальные решения в виде тарантула и т.д. Суть в том, что в случае необходимости, адаптер легко заменить, при этом внутренности приложения трогать вообще не придётся.
И это часто удобно. Пишешь фронт, нет желания распыляться на бэк. Хардкодишь в json данные, либо пишешь генератор нужных json-данных и всё.
Цена в виде небольшой когнитивной нагрузки от дополнительного уровня абстракции не сильно большая за подобные преимущества.

Да, я постоянно вижу, как в качестве примера приводят либо хранилище файлов либо какое-то key-value. Конечно для них подойдет такая схема с интерфейсом и чтобы заменить можно было потому что там 3 функции всего в интерфейсе: создать, прочитать, удалить.

Случай с базой совершенно другой, там в интерфейсе будет примерно столько же функций, сколько апишек HTTP в этом сервисе. Эта ситуация совершенно другая с точки зрения принципа Low Coupling - High Cohesion. В случае хранилища файлов внешних связей мало, так что можно его отделить, а в случае базы внешних связей будет максимально возможное количество, поэтому и отделять его - плохая идея.

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

Предположим, необходимо разработать систему редактирования, например, кабинет управления магазином на маркетплейсе. Для демонстрации возьмем сущность "продукт". В репозитории будет всего 4 метода: прочитать, добавить, удалить и сохранить. Процесс внесения изменений будет выглядеть следующим образом: получить продукт (из репозитория в сервис) -> внести изменения (на уровне сервиса) -> сохранить продукт (из сервиса в репозиторий). Для чтения создается отдельный интерфейс: получить один "продукт", получить фильтрованный список "продуктов" (с постраничной навигацией, поиском по атрибутам и т.д.). Подключаем индексацию. Этого достаточно для формирования необходимого интерфейса для юзкейсов менеджера магазина, связанных с управлением сущностью продукт. По такой же схеме внедряем агрегаты "опция продукта", "картинка опции продукта", "инвентарь продукта" и тд.

Далее необходимо разработать систему потребления, например, каталог маркетплейса с точки зрения пользователя. Для демонстрации возьмем ту же сущность "продукт" в контексте каталога. Допустим, пользователь переходит к продукту по прямой ссылке. Здесь нужно загрузить сразу данные о производителе, изображения, информацию о наличии, комментарии к продукту и т.д. Вместо того чтобы выполнять десятки джойнов по ключам, можно предагрегировать всю эту информацию по ключу продукта в асинхронном процессе. Предагрегация подразумевает формирование нового ... агрегата / агрегатов. Поэтому методы останутся теми же: получить, сохранить изменения, добавить, удалить. Для чтения создается отдельный интерфейс: получить один "продукт каталога", получить фильтрованный список "продуктов каталога" (с постраничной навигацией, поиском по атрибутам и т.д.). Подключаем индексацию. Настраиваем агрегаты под юзкейсы, и в итоге получаем единый интерфейс для работы с базой.

Теперь, если Вам потребуется перейти на другую базу данных полностью или частично (например, перенести только продукты каталога), достаточно будет внести изменения только на инфраструктурном уровне, что никак не отразится на коде сервисов.

Хотя этот принцип соблюдается даже в более простых конфигурациях. Грубо говоря, можно отделять мух от котлет, а можно и нет.

2 слоя хоть как-то можно понять, а когда делают 4 слоя а потом еще и оказывается что у нас-то микросервисная архитектура, а давайте у нас будет этот сервис по gRPC ручки выставлять и будет отдельный интеграционный сервис, который по gRPC всю инфу собирает, аггрегирует и отдает по HTTP и в этом итгрегационном сервисе еще 3-4 стоя - это уже чересчур. 8 слоев (и 8 мапперов на каждый объект) чтобы json из базы выдать выглядит не очень оптимальной схемой.

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

  1. Явный DI > Wire/fx/другие подобные решения.
    Для инициализации зависимостей можно делать отдельные пакеты/подпакеты в cmd, если уж такой большой проект, но только не IoC-контейнеры, с которыми выстрелить в ногу легче простого. (Как минимум в Go)

  2. Ну и не понимаю, хоть убейте, зачем для структурированного логирования использовать что-то иное кроме zap, который почти что стандартом стал.

  3. Также не знаком с gin, однако судя по приставке Should у вас свалится паника, если не смаппится запрос. Может проще адекватно вернуть ошибку? Паника - это про фатал-кейсы, тут у нас просто плохой запрос, т.е. ошибка данных, но никак не фатальная ошибка, способная всё повалить.

  4. И зачем логировать ошибку в сервисе, если её можно пробросить на самый верхний уровень и уже там корректно обработать в одном месте как нужно?

  1. Мы на стандартный slog перешли год назад, пока норм, но у нас всего 20-60к RPS и логи либо ошибки либо дебаг включаем руками

  1. Да, явный вполне хороший вариант. А я уже как-то сдружился с IoC и не хочется менять подход, плюс спокойней когда знаешь что при надобности можно все же легко подменить серисы. Проблем с ним на практике не возникает.

  2. Да просто удобный интерфейс для настройки, логирования, и по умолчанию удобный вывод сообщений. А других требований к логгеру особо и не предъявляю.

  3. Паники не будет. Но ShouldBindQuery() возвращает ошибки. Так что да, тут надо обрабатывать, я пожалуй допишу в этом примере попозже.

  4. Да, лучше наверх прокинуть.

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

Ну и если логгер влияет на скорость — с логгером что-то не так. Он должен быть абсолютно асинхронным и, желательно, за брокером.

Когда надо протестировать более менее крупный кусок логики, который использует несколько источников для данных, то уже неудобно, надо мокать несколько вещей. А если данные запрашиваются несколько раз за вызов, то надо еще и в правильном порядке подсовывать нужные данные. Это еще неудобнее. Если в процессе делаются и сохранения и выборки на основе сохраненного, то по тестам это тоже усложнение. А если в код, который покрыт такими тестами, вносятся изменения, например, добавляется запрос данных из нового источника, то и тест приходится ворошить. А он уже навороченный. И править тест уже прилично сложнее чем основной код, получается что на задачку тратим 1 час, и на тест еще 4. И при этом код который пишет/читает из базы сам по себе не особо сложный, там мало где ошибиться можно. Поэтому на мой взгляд проще отделять код который выполняет какие то сложные вычисления (получает уже выбранные из БД данные, обрабатывает их, и возвращает результат), и покрывать тестами его.

А чтобы логгер влиял на скорость, лично я такого ни разу и не замечал. Дело в том, что библиотеки логгеров бывает сравнивают себя с другими библиотеками и в сравнении делают упор на скорость работы. Зачем это делают - мне правда не понятно. Логов либо пишешь относительно мало чтобы визуально было легко разобрать что к чему, либо складываешь их в какое-то хранилище чтобы потом в нем можно было искать нужное. Но узким местом тогда уже становится не логгер, а запись в хранилище.
Да и если 1M записей в минуту в stdout пишется без проблем, не создавая значительную нагрузку на процессор или память, то вопрос скорости тут и не должен подниматься на мой взгляд.

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

узким местом тогда уже становится не логгер, а запись в хранилище

Если лог асинхронный, никто не становится узким местом: fire and forget.

самая популярная gomock: https://github.com/uber-go/mock

Но тут проблема не в моках, а в большом блоке кода с большим числом зависимых вызовов. В го люди плохо умеют в декомпозицию и тестирование, так что не удивительно, что строгий экспект мока - мешает.

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

Я посмотрел на библиотеку выше — и увидел навскидку две очень серьезные проблемы (насколько я понял по поверхностному взгляду): глобальность моков и нечеловеческое количество бойлерплейта.

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

глобальность моков и нечеловеческое количество бойлерплейта.

что за глобальность? бойлерплейт генерится тулзой.

Как быть, если у меня ну пусть два гринтреда, в одном мне нужен один мок, а в другом — другой?

навскидку проблема непонятна. давайте конкретный пример.

бойлерплейт генерится тулзой

А изменяется?

навскидку проблема непонятна

На каждый активный заказ запущен гринтред. Как проверить выполнение контракта для заказа №111111, не меняя поведение остальных пятисот заказов? Только не нужно мне предлагать юнит-тест с одним заказом в вакууме, он ничего не тестирует, кроме success path в тепличных условиях.

А изменяется?

Да. Пишешь команду генерации моков в go:generate и погнали.

Можно хоть на автосохранение триггер повесить.

Только не нужно мне предлагать юнит-тест с одним заказом в вакууме

Unit тест тут не нужен. Вам нужно писать интеграционный или функциональный тест.

Вам нужно писать интеграционный или функциональный тест.

Обожаю людей, которые точно знают, что мне нужно. Ни интеграционный, ни — тем более — функциональный тест тут вообще никак не поможет.

Повторяю: мне надо отследить конкретную потенциальную гонку в высококонкурентной среде. Проверить последовательность вызовов и их аргументы.

Что такое гриндтред? Это некий канал по которому идут заказы?

Что за контракт? Это некая спецификация?

Можете накидать в коде пример?

Что такое гриндтред?

Гринтред. Горутина в го.

Что за контракт? Это некая спецификация?

Интерфейс, например. Да, в общем случае — спецификация.

Можете накидать в коде пример?

Пример чего? Запуска 500 горутин?

Правильно ли я понимаю, что вы запускаете 500 гороутин, но вам надо проверить одну конкретную?

Да. Я это написал в первом комментарии в этом треде.

Пример чего? Запуска 500 горутин

Ну а сами как думаете? )

Никак не думаю. Если вам непонятна постановка задачи в том виде, как я ее озвучил, я очень сомневаюсь, что код что-то прояснит.

Давайте просто забудем. Я всё равно писать на го не планирую, это был чисто академический интерес. А как сие сделать в языках, которые мне интересны — я и сам прекрасно знаю.

отследить конкретную потенциальную гонку в высококонкурентной среде

Ох, это не просто. Как это делаете?

На го — не знаю, я в самом первом комментарии это сказал.

В эрланге/эликсире, с которыми я преимущественно работаю — mox использует nimble_ownership, и то мне пришлось в обе библиотеки патчи слать.

Обожаю людей, которые точно знают, что мне нужно. Ни интеграционный, ни — тем более — функциональный тест тут вообще никак не поможет.

Юнит тесты не предназначены для выявления data races и правильности работы многопоточки, вам все правильно говорят.

На каждый активный заказ запущен гринтред. Как проверить выполнение контракта для заказа №111111, не меняя поведение остальных пятисот заказов?

у вас в этом заказе меняется поведение в треде, и вы хотите проверить что он сделает определенные вызовы? Непонятно зачем все-равно привязываться к конкретному green thread, надо оценивать итоговый результат. опять же без кода сложно советовать как это сделать. Управление поведением мока на основании thread id даже в других языках выглядит как треш, уж простите.

Race conditions не отслеживаются через юнит тесты. Через юниты вы можете отследить конкретные ветки выполнения. Для этого достаточно 1 заказа. Не его задача тестировать как это будет работать под нагрузкой, или в системе с 1 ядром вместо 8. От того что вы проверите что у вас все вызывается как надо, в реальной среде лучше не станет, потому что гонка это ошибка проектирования доступа к данным. А следовательно проверять надо под нагрузкой: создавать 5к заказов, запускать обработку и проверять в конце что сошёлся дебет с кредитом. И так несколько итераций.

Race conditions не отслеживаются через юнит тесты.

В акторной модели, например, отслеживаются с легкостью.

проверять надо под нагрузкой: создавать 5к заказов, запускать обработку

Да, я именно это и делаю.

проверять в конце что сошёлся дебет с кредитом. И так несколько итераций.

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

И править тест уже прилично сложнее чем основной код, получается что на задачку тратим 1 час, и на тест еще 4.

побочка высокого code coverage. многие думают что высокий % coverage это панацея. тебе прилетает баг, а на это написан тест и даже не один, в результатах которого полная чушь. потому что часто тесты пишутся так: достается результат после прогона модуля и подставляется в expected. покрытие 80%, все довольны. можно поплакать и договориться делать хорошо, но человеческую природу не поменять. единственное что юниты хорошо делают - противостоят изменениям в программе.

Поэтому на мой взгляд проще отделять код который выполняет какие то сложные вычисления (получает уже выбранные из БД данные, обрабатывает их, и возвращает результат), и покрывать тестами его.

у меня примерно так: алгоритмы, core часть покрыта по максимуму. все завернуто в моках. отдельно интеграционные тесты на репозиторий, где поднимается база и просто прогоняются сценарии работы с репозиторием. контроллеры где возвраты ошибок - минимум тестов. слишком много затрат, слишком мало выхлопа с учетом того что код там меняется редко. но большинству конечно проще, когда есть догма вроде тестируй 70% и станешь молодцом.

Когда вы пишете юнит-тесты, важно понимать, что именно вы тестируете. Для этого необходима декомпозиция: нужно четко разделить вашу бизнес-логику и инфраструктуру. В юнит-тестах имеет смысл тестировать только бизнес-логику, а инфраструктуру отделяем в отдельные модули (например, доступ к службам по HTTP, gRPC и т.д.).

Таким образом, в юнит-тестах не должно быть сложных моков. Чем больше моков вы используете, тем больше сигналов о том, что вы смешиваете ответственности в коде. Все программирование сводится к трем основным этапам: получить данные (input), обработать данные (process), записать данные (output). Отделите обработку (process) в отдельную функцию, на которую следует обратить особое внимание при написании юнит-тестов. В самой "функции оркестрации" на уровне служб фиксируйте логический источник, из которого получаете данные (сервис/метод) и какие данные (контракт), и логический источник, куда записываете данные (сервис/метод). Для этого можно использовать инверсию зависимостей. Также можно зафиксировать корректную реакцию функции оркестрации в случае, если не удалось получить данные (если у приложения есть какая-то логика действий в этом случае).

утрированный пример инверсии зависимотей на c#
public interface IMyUseCaseInputService { MyUseCaseData GetData(params); }
public interface IMyUseCaseOutputService { void SaveData(data); }

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

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

Так что по сути рецепт такой: юнит тестами проверяем только ключевую бизнес логику связанную с обработкой данных (process). В гексагональной архитектуре это и есть центр гексагона. А все остальное (адаптеры) интеграционными. А еще лучше - все вместе интеграционными области e-2-e, но это уже если ресурс есть.

На инфраструктурном слое тоже надо unit тестами обмазываться, особенно вызов сторонней библиотеки. Чтоб изменение ее поведения не стало сюрпризом.

На бизнесслое unit тестами так же надо обмазывать возврат обернутых ошибок и nil, empty значения. И пограничные состояния.

Тейк про юнит тестирование чужих библиотек я не понял. Т.е. создатели библиотеки обмазывают ее юнит тестами, а потом потребители библиотеки тоже обмазывают ее юнит тестами?

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

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

Т.е. создатели библиотеки обмазывают ее юнит тестами, а потом потребители библиотеки тоже обмазывают ее юнит тестами?

Нет, мы с помощью unit теста фиксируем ее поведение. В самом тесте нет вызова библиотечных функций, тестируется традиционно по методу "черного ящика", но результат должен отражать какое либо действие библиотеки.

Например мы используем сторонний sql builder, в тесте мы может проверить sql запрос.

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

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

Я автор и майтнтейнер более 10 библиотек, из которых минимум 4 используются сообществом довольно активно. Сломать API при фиксации версии 1 — это был бы верный путь к забвению и порицанию.

Все библиотеки, которые я использую, ведут себя так же.

Бамп версии 1 — это фиксация функциональности, а не легитимная возможность всё поломать.

Это не Го а какоето извращение. Тут ничего нет из фишек го. Впечатление что это код на пхп

А вы про пхп говорите будто бы в негативном контексте. А язык между тем довольно хороший. И объектная модель там хорошая, и код там можно писать наглядно и аккуратно. Про пхп плохо говорят от того что много разработчиков писали лука-код. Да и объекты там не сразу появились. Но сейчас это один из удобных языков. Если бы не производительность, то вообще был бы огонь-язык.

Дело не в пхп и не в хороший/плохой, а в том что вы пишете на го как на пхп

Вижу горм и di ставлю дизлайк. Люди пытаются писать на го как на пхп/жаба/кресты и тащат всякие гениальные паттерны. Если ваш язык так хорош че вы на го идёте а не на своем пишете. Невозможно потом поддерживать и расширять.
Пример буквально с прошлой недели: кто-то умный пару лет назад сделал тот самый круд через горм. Теперь оказалось что надо сущности создавать не по одной а кучей и по несколько тысяч. В итоге вместо добавить один метод и потратить день тратим неделю на переписывание всего на pgx. Как будто sql это латынь или китайский, чтобы на нём писать не мочь.

У нас тоже была интересная история. Пришел парень, причем тоже пару лет назад, может один и тот же (шутка)? И сказал что наш DI - полное Г, что на го так не пишут. И сервисы слоеные наши тоже какое то Г, что так не надо делать, что это "не go-way". А я люблю дать свободу человеку, когда он идеи толкает. Ни у говорю - ок, сделай кусок кода под свою задачку как считаешь нужным. Может и вправду будет лучше и мы тогда возьмем это и намотаем себе на ус.

Он интересно в итоге сделал: накопировал в несколько мест создание экземпляров вместо того чтобы 1 раз описать в DI. А данные из БД прямо в мапки грузил. Вот тут я и вспомнил с ностальгической слезой PHP4 и мы с ним на этом распрощались.

а почему там требовалось создавать в разных местах экземпляры? обычно в main все создается и прокидывается как зависимости. про мапы в базах - тоже перебор конечно.

но в целом идея сотрудника понятная: много кто приходит писать в го с пхп или джавы и приносят туда весь этот аттракцион развлечений. горм вообще отдельная история, с которой я намучался в течение года. мигрировать базу, учитывая что у тебя code first - боль. автомигратор - шляпа для POC. а писать вручную sql для миграций выглядит как костыль. единственный вариант использовать atlas, но и там тоже все далеко неидеально. и это не считая приколов с самой либой. c wire я тоже поработал - остался не в восторге, правда это было несколько лет назад.

а почему там требовалось создавать в разных местах экземпляры?

Да вот и я тем же вопросом был озадачен.

вручную sql для миграций выглядит как костыль

А почему как костыль, вроде самый наглядный и контроллируемый вариант. Там же не нужна универсальность, вряд ли понадобится менять СУБД и переносить весь проект на новую БД. Atlas как то страшновато использовать. Обновишь этот пакет и мало ли чего он решит добавить в табличку весом с половину диска.

Костыль потому что теряется смысл ОРМ - абстракция от бд, в идеале тебе вообще должно быть все равно что у тебя там. Ты вынужден писать форейн кей в sql и правильно понимать как его после этого разметить в коде, чтобы у тебя сработал прелоад. Это кстати тоже отдельная тема, когда ты делаешь разметку тегами, а оно не работает как надо. Я за то чтобы либо использовать орм и не лезть руками в базу, либо лезть в базу но писать запросы руками или билдером.

Для меня основной выигрыш от ORM в том ,что она избавляет от надобности писать маппинги сущностей кода в сущности базы. А билдеры запросов это уже приятное дополнение. Или не очень приятное, как например в питоновском django)

А разве горм избавляет от этого? Вы ведь все равно привязаны к базе/либе и определенным форматам? Может быть пример у вас есть? Я возможно просто что-то позабыл уже.

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

type User struct {
	Name string
}

func GetUsers(name string) ([]User, error) {
	var items []*User
	return s.db.Find(&items)
}

ЕМНИП sqlx такое умеет, если ему помочь тегами.

type User struct {
	ID       int    `db:"id"`
	Name     string `db:"name"`
	Email    string `db:"email"`
	Age      int    `db:"age"`
	IsActive bool   `db:"is_active"`
}
// somewhere in code
err = db.Get(&user, "SELECT id, name, email, age, is_active FROM users WHERE id = $1", insertedID)

в целом пойнт понятный. с гормом чуть попроще.

sqlx > gorm, умеет такое же, но не приносит все минусы ORM-ок. Ещё и квери билдер удобный

Сам использую SQLX, однако когда нужно обновить динамическое число полей, начинается конкатенация, что сильно меня удручает. Какой вы используете квери билдет в таком случаи?

Всегда люблю спрашивать, люителей обычно в main всё создавать. Какой у них code coverage тестами их main'a?

а что в main тестировать если там создаются реальные экземпляры? Это решается интеграционными тестами либо e2e.

Справедливости ради, main покрывать тестами довольно бессмысленно: оно же либо запустилось, либо нет. Ошибки обычно возникают в бизнес-логике, в высоконкурентной среде, в гонках, в алгоритмике. Но уж никак не в инициализации.

В инициализации тоже возникает. Но тут есть ещё момент, что у го есть особенность технологическая - позволяющая запустить обычным test suite сервис с его интеграционными зависимостями. И когда у вас есть DI (даже ручной и статический) - то го позволяет проверить и это тоже.

Но борцы за main в го предпочитают 2000-3000 строк кода в одной функции написать и ловить проблемы не пробе.

Что можно инициализировать тысячами строк? Вселенную?

Даже инициализация Солнечной системы должна укладываться в сто сорок строк, примерно.

Вопрос весьма сложный. Я сам всегда недоумеваю, но тем не менее.

Самое маленькое что я видел 900 строк кода.

Я помню, как на заре появления докера, еще до того, как исходный код открыли, в сеть просочился кусочек инициализации чего-то там, с двузначной вложенностью условных операторов. Лет 10 назад, наверное, или типа того.

Они это наверняка подчистили уже, но я с тех пор запускаю докер только, если прям совсем припрет — и только в песочнице :)

у го есть особенность технологическая — позволяющая запустить обычным test suite сервис с его интеграционными зависимостями

Ну в эрланге/эликсире это дефолтное (и труднообходимое) поведение; тестовые фреймворки обычно позволяют запускать сразу кластер из нескольких нод (по желанию — в разном окружении).

В го нет аналога того что есть у эрланга с эликсиром, но руками можно собрать. Только не из main функции. Потому и неплохо инициализацию утащить чуть в сторону и использовать потом тем же способом, что вы и описываете.

Требует культуры, у любителей мейнов с ней беда увы.

Требует культуры, у любителей мейнов с ней беда увы.

человек, у которого инициализация в одной функции занимает 3к строчек рассказывает про культуру?

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

Никакой, смысл тестировать мэйн?
Раз так хочется покороче - разбил инициализацию зависимостей по подпакетам (условный cmd/{название-проекта}/bootstrap/*, и там инициализируешь всё, что нужно, а сами Init-ы дёргаешь в мэйне. Можешь даже тесты прикрутить, кто тебе мешает

Например, как-то так
Например, как-то так

с учетом того что у них там main под 3к - я бы сказал что последнее за что нужно воевать и при этом еще кого-то поучать - где лежит код инициализации, в main или отдельном пакете. учитывая что это переносится в отдельный пакет в пару минут. в нормальных сервисах main не нуждается в тестировании с code coverage, потому что тестировать как у тебя поднялось приложение - достаточно тривиальная задача, которая даже тестом не нужно решать, а банальным CD. потому что внезапно есть еще слой конфигурации, в котором тоже могут быть ошибки.

история из жизни: у нас был сервис с инициализацией в main. решили сделать интеграционные тесты через поднятие сервиса. вынесли целиком функцию контент из main в пакет app.init. в тестах вызывает app.init. стало ли это поводом всегда писать инициализацию в отдельном пакете? нет. потому что не везде есть интеграционные тесты. а учитывая что это занимает минимальное количество времени на рефакторинг - бесмысленный спор ни о чем.

Если ваши тесты не дают информацию о зонах доступности кода (code coverage по простому), то ваши интеграционные тесты усложняют жизнь разработчику не давая ничего.

Если запуская ваши интеграционные тесты вы не можете поставить сервис на брекпоинт не делая ничего дополнительно (кроме запуска теста в IDE) - то ваши тесты не дают разработчику ничего.

Слой инициализации важный компонент интеграционного тестирования. И в этом плане достижимость эффективной работы в го такая же, как и в условной Java. Для этого не нужна магия. Для этого нужно научится - писать тесты для слоя инизициализации - потому что каждый такой тест в итоге становится интеграционным, оставаясь при этом, практически unit-тестом.

То что вам что-то кажется бессмысленным отражает ваш узкий взгляд на проблему. Как и тех, кто считают main очень удобной заменой DI контейнеру или ненужным к тестированию. В итоге ваш "простой" подход оказывается лишь более сложным и громоздким в поддержке и эксплуатации.

Мне вообще ничего не мешает иметь 100% codecov где угодно. С этим проблема почему-то только у любителей всё в main загружать.

Смысл есть всегда. По статистике где-то на 100-1000 строк кода (в зависимости от уровня исполнителя) есть 1 баг. Если у вас 2000 строк кода в мейне, у вас там минимум 2 бага да обнаружаться. Особенно при длительной поддержке. Особенно при высокой цикломатической сложности, которая свойственна авторам кода на go (максимально на что я выкрутил линтер в этом плане составляло - 1400, на минуточку).

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

Тут беда в разработчиках. DSL можно сформировать в го, но культ примитивизма приводит к тому, что делая "проще" получается сложнее.

Ну, ожидать от разработчиков, что они будут строить свои DSL — наивно, по меньшей мере.

Даже в руби, где DSL строится буквально не приходя в сознание, он остался уделом фреймворков и библиотек. В проектах его используют исчезающе редко.

справедливости ради горм умеет в батчовые вставки. чем не устроило?

Батч на запись 7к достаточно крупных сущеностей в горме + постгрес стабильно не отрабатывает. Ну то есть говорит что успех а успеха нет. Плюс медленно.

Если много данных передаете через плейсхолдеры (?, :name), то проблемы с производительностью будут с любой либой. В таких случаях приходится что-то придумывать.

Почему:

func (s UserService) GetUsers(name string) ([]entity.User, error)

А не:

func (s UserService) Users(name string) ([]entity.User, error)

Ведь в golang не принято getter обозначать префиксами Get.

Зачем тут указатель?

var items []*entity.User

Чревато багами.

Вы куда пишете? Не вижу io.Writer.

...
BadRequestJSON(ctx, err.Error())
...
ServerErrorJSON(ctx, "Something went wrong")
...
SuccessJSON(ctx, list)
...

Старт 100 проектов за 5 лет, скорее минус.

Настоящий плюс когда работаешь 5 лет на поддержкой и сопровождением 100 проектов. Над которыми поработало уже 100500 разработчиков со своим видением. Вот это настоящий опыт. У меня пока 20 таких средних проектов за 5 лет и два крупных.

Вы настоящий оракул!

Sign up to leave a comment.

Articles