Comments 20
Спасибо за перевод.
Код конечно слабенький (человек не понимает ООП в Go, не использует интерфейсы почти нигде), но это гораздо, гораздо лучше обычного "давайте писать на Go как на жалком подобии Java с кучей пустых интерфейсов и фабрик фабрик".
Жду вашу статью о том как лучше писать на Go
У вас точно в фильтре стоит "показывать статьи с рейтингом -50"?) Не думаю, что он будет выше. Аргументы в духе "так написан net/http и он работает хорошо (в общем случае)", "любой кусок легко протестировать", "мы ни разу не теряем информацию о типе (типобезопасность)" никогда не помогают, и меня всегда сливают. Дело в отношении к привычным паттернам и анти-паттернам, первые считаются обязательными априори (но неприменимы к Go), а вторые порой применять надо (хоть и осторожно). Это связано с тем, что авторы по сути наплевали на десятилетия развития ЯП и "переизобрели" что-то заново, откинув многие "достижения" (например исключения и наследование, которое сильно повлияло на полиморфизм, а значит и на внутреннее устройство типов).
Это связано с тем, что авторы по сути наплевали на десятилетия развития ЯП и "переизобрели" что-то заново, откинув многие "достижения" (например исключения и наследование, которое сильно повлияло на полиморфизм, а значит и на внутреннее устройство типов).
Вы посмотрели кто авторы Go? Если Роб Пайк и Кен Томпсон не умеют « в разработку языка» , то может подскажете кто умеет?
Идея убрать наследование и оставить только «методы расширения» и интерфейсы на самом деле классная, так как наследование практически не используется в современной разработке. Почему- это отдельный вопрос.
Опять таки, MS с его комбайном .Net очень быстро скопипастил подход Го и впихнул его в .Net. Но как часто у них бывает через одно место. Так что теперь в C# вообще невозможно понять кто кого вызывает и где что определено.
Зачем вам интерфейсы в HTTP сервисе, в части которая отвечает за обработку запросов?
Не понял отсылку в сторону C#, можно подробнее?
В C# относительно недавно добавили методы расширения. Теперь можно фактически добавит свой метод к любому классу из стандартной библиотеки.
Этот концепт они похоже «украли» из Go - где это единственный метод объявления челнов класса.
Поэтому говорить что в Go не осилили сделать нормальное наследование не стоит.
Если этот подход MS затащили в свой «комбайн».
Классическое наследование как его реализовали в C++ показало ряд недостатков: невозможность множественного наследования.
В C++ это можно сделать, но есть шанс создать ромбовидную зависимость, когда
А<-B
A<-C
B,C <-D
класс D наследует поля/методы А которые есть в B и C
Чтобы этого избежать в C++ в случае множественного наследования рекомендуют наследовать только от абстрактных классов.
Позднее это породило концепцию Interface , если не ошибаюсь, ее как раз в С# MS и реализовал одним из первых.
Концепция Interface предоплате что наследуются не данные а поведение.
Дальше возникла проблема: интерфейс не имеет реализации, поэтому вам сначала надо опередить интерфейс, а затем описать его реализацию.
Что ведёт к дублированию кода. Во многих случаях вы описываете интерфейс чтобы потом создать единственную реализацию.
Соответственно в Go сделали следующий логической шаг- если данные не наследуются, то и само понятие наследования не существуют.
Поэтому есть структура и определен набор методов для работы с ней.
Фактически интерфейс.
Все описывается в одном месте.
Т.е. создали структуру
Cat
Определили методы:
Run, Jump
позднее надо добавит собак:
Определили структуру dog
Добавили методы Run, Jump
Собаки от Кошек не наследуют, общий базовый там Animal создавать не надо.
При этом Cat и Dog имеют одинаковый интерфейс «из коробки»,
Что позволяет работать с ними одинаково, пока они только бегают и прыгают :)
Неожиданно было такое услышать. Методы расширения в C# добавлены начиная с версии 3 языка. Как мне подсказал гугл, это 2007 год. Го релизнули в 2009... Если уж говорить, кто у кого позаимствовал (хотя может где-то эта концепция была реализована и ранее, а придумана наверняка еще задолго до этого). Про дублирование кода аргумент, как по мне, сомнительный. Возможно, если у вас одна реализация, то и интерфейс не нужен. Зачем абстракция ради абстракции. Но если уж мы создали интерфейс, то он может пригодиться нам в тестах, а это уже +1 реализация интерфейса.
Вы посмотрели кто авторы Go? Если Роб Пайк и Кен Томпсон не умеют « в разработку языка» , то может подскажете кто умеет?
Я нигде этого не писал, совсем. "Достижения" в моём сообщении потому и в кавычках, что я их таковыми не считаю (как и Пайк, чьи статьи и книжки я читал ещё для Си, который он предпочитал в эпоху до Go). Вы не в ту сторону тут воюете.
Зачем вам интерфейсы в HTTP сервисе, в части которая отвечает за обработку запросов?
Я нигде этого не писал, совсем. Они не нужны в обработке запросов, они нужны в конструкторе сервиса.
Зачем вам интерфейс в конструкторе?
Это то, как мы в Go делаем decoupling. Есть конечно те, кто по привычке с условных шарпов делают инстанс интерфейса и таскают его туда-сюда, но они обычно профнепригодны.
Хороший пример в стдлиб bufio.Scanner (https://pkg.go.dev/bufio#NewScanner). Конструктор принимает reader, и сканер может работать хоть поверх файла, хоть поверх stdout, хоть поверх http request body, да хоть поверх обычной строки, из которой можно сделать reader с помощью пакета strings.
В итоге мы никак не зависим от конкретной реализации reader и буквально можем переехать на другой 1-2 строками кода. Это правило работает и с бизнес-кодом. Хотим сменить логгер или базу на другую? - Отлично, меняем строку импорта на другую реализацию + меняем строку инициализации логгера или базы и передаём уже другую реализацию интерфейса в конструктор. Кто-то скажет DI для бедных. Так и есть ?
Хотим сменить логгер или базу на другую
На мой взгляд за эти вещи пакет net/http и не должен отвечать, за это должна отвечать обвязка которую над ним пишут. Да и в целом часто вижу подход в котором создаётся отдельная структура под апи и там задаётся логер, который также можно изменить парой строчек. С базой тоже самое.
Хороший пример в стдлиб bufio.Scanner
На мой взгляд взгляд тут разные вещи. Сканер имеет четкую и понятную задачу и ограниченный вариант использований. Честно говоря я не особо представляю как вы хотите засунуть многообразие логеров в конструктор net/http (мб я конечно неправильно понял коммент). С базой по моему совсем без вариантов в стандартной библиотеке, помимо выбора базы нужно выбрать еще драйвер и методы базы по сути нереально подбить под один интерфейс в отличии от Reader у которого 1 метод Read в базе нельзя сделать так, чтобы был только 1 метод для всех операций без кучи указаний от пользователя. В логере еще можно конечно попробовать что-то придумать, но тоже сомнительно
Небольшая ошибочка в коде есть:cancel()
в defer
не сработает после os.Exit()
Хм, автор сказал как он тестирует приложение, вызывая run и накатывает миграции и тому подобное, но как автор после каждого вызова теста который что то добавляет в БД потом её очищает, чтобы не получить side effect от мусора оставшегося после предыдущего теста? Пересоздавать БД каждый раз дорого.
Плюс не очень понятно как работает такой подход если используется авторизация через сторонний сервис который не так то просто замокать. (Например auth0).
Для тестирования HTTP api я обычно применяю похожий подход но исключаю некоторые зависимости вроде миддлвар с проверкой авторизации. Т.е. у меня в server_test.go будет своя функция которая сформирует хендлеры как и в проде, но за исключением каких то зависимостей, плюс данная функция по мимо хендлера вернёт коннект к тестовой бд и прокинуный в сервисы репозиторий для работы с БД.
RepoDB использует тот же коннект что вернётся из функции. Затем в табличной тесте есть функция setup() которая возвращает функцию teardown() в setup я открываю транзакцию и заменяю ей исходный коннект который лежит в RepoDB. Теперь всё что я создам для теста и всё что создаст тест будет откачено в teardown() функции после завершения теста.
Мне с setup() и teardown() очень нравится такой финт:
func setup() func() {...}
func TestABC(t *testing.T) {
defer setup()()
...
}
А как же тестировать тогда race-condition и другие проблемы синхронизации разных потоков? Тут нам как раз нужна общая база и единственный инстантс бэкенда. И тогда ужу тестировать так в многопоточном режиме, чтобы эти проблемы вылезали в тестах.
Для этого можно написать отдельные тесты, плюс я не очень понимаю про какие потоки вы говорите, если это относиться к параллельным запросам к апи для изменения данных то тут нормально может справиться БД если запросы и ограничения на таблицы выставленны верно. В моем примере инстанс бд будет один. Тесты должны быть изолированны друг от друга, мне кажется это одно из основных правил написания тестов.
База не защищает полностью от ошибок с состоянием гонки.
Приведу пример.
Апи1. Поиск объектов. Передаем список желаемых ключей, возвращает подходящие объекты
Апи2. Создание нового объекта.
Тест. Создаем новый объект, проверяем что его можно найти одному из ключей. Тест успешно проходит в однопоточной версии, но валится (возвращает неправильный объект ) в многопоточной ( когда все тесты запущенны конкурентно ).
Что оказалось. Запрос на поиск объекта не учитывал один из ключей, то есть фактически запрос был
`select item from items limit 1;`
Это прекрасно работало для только что созданного объекта, поскольку бд по умолчанию возвращала именно его, но уже не работало когда множество объектов одновременно создавались разными потоками.
База не защищает полностью от ошибок с состоянием гонки.
Сценарии тестирования под такие случаи должны отдельно писаться, в зависимости от того, что нужно.
Приведу пример.
Апи1. Поиск объектов. Передаем список желаемых ключей, возвращает подходящие объекты
Апи2. Создание нового объекта.
Возьмём ваш пример, если создание объекта гарантированно начинается до получения запроса на выборку, то это можно решить при помощи транзакций и локов в бд. Если запрос на получение объекта пришел раньше начала создания объекта, то база вернёт результат.
Это прекрасно работало для только что созданного объекта, поскольку бд по умолчанию возвращала именно его, но уже не работало когда множество объектов одновременно создавались разными потоками.
Тут уже нужно смотреть на конкретный тест, т.к. есть вероятность что данные просто еще не были созданы и само собой бд не могла их вернуть, если это делалось без отдельных транзакций то по сути это нормальная работа бд
Как я пишу HTTP-сервисы спустя 13 лет работы с Go