README
¶
Проект Calendar
Пример тестовой API
| Method | URL | Descrition |
|---|---|---|
| POST | /api/v1/signup | Создать пользователя и получить для него токен |
| POST | /api/v1/signin | Получить токен для существующего пользователя |
| POST | /api/v1/verify | Получить информацию о пользователе по токену |
| PATCH | /api/v1/users/{id} | Обновить данные пользователя |
Структура проекта
├── cmd -- точки входа в приложение
├── internal
│ ├── apperrs -- основные типы ошибки приложения
│ │ └── errors.go
│ ├── application -- сборка компонентов приложения / ручной DI
│ ├── controllers -- обработчики входящих запросов
│ ├── auth -- домен авторизации
│ ├── users -- домен с пользователями
│ │ └── repository
│ ├── config -- конфигурация
│ ├── databases -- обертка над пакетов sql
│ ├── logger
│ └── transport -- http-server / клиенты http, gRPC, если есть
├── migrations -- миграции БД
├── scripts -- вспомогательные скрипты
Запуск приложения
Используются следующие переменные окружения:
docker-compose up -d
# дождаться завершения миграций
go run cmd/main.go
Разработка проекта
Для разработки могут понадобиться дополнитльные инструменты. Их можно установить через:
# инструменты будут скачены в папку ./tools/bin
make install-tools
Вопросы для ревьюера:
HW1
-
Q: Когда следует использовать оборачивание ошибок при помощи директивы
%w, а когда нет? Правильно ли я использовал эту директиву в коде?A: Директива
%wиспользуется для оборачивания ошибок, когда нужно сохранить информацию о первопричине ошибки.Так как в Go ошибка, это всего лишь интерфейс возвращающий строку, она обычно не несет никакого контекста о месте, где она возникла (в этом разница с привычными Exceptions). Поэтому мы сами добавляем нужный нам контекст о возникновении с помощью враппинга. Его можно и нужно делать везде, где важно показать в каком месте и при какой операции возникла ошибка. Не нужно делать там, где мы используем общие стандартизованные ошибки (см. Sentinel Errors).
В целом ты везде использовал
%wправильно. Подсвечу лишь, что в некоторых компаниях принято использоватьerrors.Wrapдля той же операции.Nit: правильно все же называть это не директивой, а глаголом форматирования, так как эти штуки пришли еще из C и ему подобных.
-
Q: Нужно/можно ли использовать кастомные ошибки на текущем этапе разработки? В каких случаях это будет уместно?
A: Если речь о создании своего типа, который будет имплементировать интерфейс
Error, то я не вижу смысла делать это сейчас. Обычно это делают там, где не хватает стандартного функционала ошибок, тогда их делают в виде структур, добавляют метрики, трейсы, вкладывают одну в другую и т.д. Зачастую те, кто пытается играться в ошибки-структуры просто не отвыкли от классических исключений и пытаются затащить этот механизм в Golang (а зря).Если же речь про вынос переиспользуемых ошибок в Sentinel Errors, то да, это вполне можно делать уже сейчас, на твоё усмотрение. Я только за.
-
Q: Почему в бойлерплейте проекта контекст для сервера передается в виде параметра closer функции stopHTTPServer в пакете application? Выглядит так, что он вряд ли будет часто меняться и можно его переместить в пакет server.
А: Там по сути используется обыкновенное замыкание. Контекст передается не для сервера, а для функции, закрывающей соединение с сервером. Эту функцию мы вызываем в application, объявляя и вызывая анонимную функцию на лету (в JS это называется IIFE - immediately invoked function expression). Из этой анонимной функции возвращается новый контекст, созданный специально для завершения работы сервера с отдельным таймаутом. Обычно это делается, когда мы хотим дать время на закрытие ресурсов. В этом случае мы не используем корневой контекст и создаём новый.
Где именно создавать новый контекст большой разницы нет, но я догадываюсь почему так сделал Виталий (автор бойлерплейта). Потому что обычно мы хотим управлять контекстом на как можно более верхнем уровне. В
main.goобычно создается самый главный (глобальный) контекст который определяет жизненный цикл всего приложения, а вapplicationв данном случае создаем побочный контекст только для одной задачи.В целом всегда желательно чтобы управление контекстом происходило снаружи, чтобы сам сервер не знал о таймауте с которым ему предстоит "закрыться".
-
Q: Контекст - пункт задания: Используется логгер и он настраивается из конфига. Вопрос: какие настройки логгера следует выносить в конфиг? По какому принципу выносить?
A: Тут нет никаких правил, но из опыта и здравого смысла могу назвать:
-
Те, которые меняются в зависимости от среды выполнения. Пример: на локальной машине удобнее читать обычные строки в stdout, в то время как для нормального сбора логов и метрик в продакшне просто необходимо использовать json-форматированные логи, т.к. это де-факто стандарт индустрии. Следовательно -> можно вынести в конфиг тип форматирования.
-
Те, которые меняются часто. Пример: уровень логгирования (хотим подебажить - врубаем DEBUG, в штатном режиме обычно работает INFO или WARN).
-
Те, которые сам считаешь нужным вынести, чтобы не создавать лишние коммиты в будущем, когда захочешь поменять что-то. Пример: я задавал формат даты-времени через конфиг, не то чтобы прям часто менял, просто по-приколу :)
В целом логгер не самая конфигурируемая вещь. Обычно настроил один раз и забыл)
-
-
Q: Правильно ли реализован и использован логгер? Рекомендации по улучшению?
A: Вполне нормально. Если захочешь, можешь добавить настройку форматирования логов в зависимости от переменной в конфиге.
-
Q: Как лучше реализовать конфиг приложения?
В описании задания написано:
Обычно для конфигурирования приложения используется несколько источников: - переменные окружения - файл конфигурации - внешние системы хранения конфигураций (consul, vault)Вопрос: Какой оптимальный способ реализации конфига в реальном проекте?
На данный момент используется:
github.com/joho/godotenv- загрузка переменных окружения из файла.env.github.com/caarlos0/env/v11- парсинг переменных окружения в структуру.
Как я понял, в боевом проекте нужно использовать оба пакета (
.ymlиди.env) + в зависимости от переменной окружения, подтягивать тот или иной.env.*Типа.env.local,.env.dev,.env.prod.A: Каждый готовит по своему, но я чаще всего видел у гоферов хранение конфигов именно в
.yaml. Чтобы попробовать такой формат рекомендую пакет viper, который упоминал выше.В целом какую бы тулзу ты не выбрал, главное чтобы в ней была возможность переопределить значения конфига через реальные переменные окружения, чтобы у них был приоритет. Потому что когда приложение разворачивается в облаке через Terraform, бежит в каком-нибудь кубере в проде, ему нафиг не сдался
.env.prodфайлик, значения переменных будут браться из Secrets кубера или другой системы, среди которых consul, vault указанные выше.Собственно viper так и работает (ну или его точно можно так настроить), что если переменная окружения не задана, то юзается значение из
.yaml, а если задана, то приоритет у неё. Она буквально перезаписывает это же значение из конфига. -
Q: На текущий момент запуск приложения golang не докеризируется и приложение запускается напрямую на хосте. Это распространенный подход для локальной среды разработки или лучше добавить использование
Dockerfileвdocker-compose.yml? Или жеDockerfileздесь нужен только для запуска в продакшен? Хотелось бы узнать про это подробнее. Какие есть практики касательно локальной разработки и докера? Можно ли разрабатывать полностью в докере? Плюсы и минусы?A: Для локальной разработки норм, но конечно стоит упаковать аппку в докер, чтобы:
- Её можно было запустить в другой среде, даже без установки golang-компилятора.
- Убедиться что все правильно билдится и приложение нормально взаимодействует с другими компонентами внутри докера.
- Попрактиковаться с докером)) На работе без него никуда в 2k24.
Локально полностью в докере разрабатывать можно, но это проще делать с интерпретируемыми языками типа python, js, так как там при разработке меняются только файлы с исходным кодом. В случае же Go и других компилируемых языков, исходный код как раз обычно не используется в результирующем контейнере, а используется скомпилированный исполняемый файл. А значит при каждом рестарте после правок у тебя будет заново происходить не только компиляция прилы, но и билд контейнера.
Плюсы:
- предсказуемое окружение, если работает в докере -> работает везде (на 99.9%)
- не зависишь от установленной версии Golang, можешь вообще его не устанавливать)
- не скачиваешь зависимости локально, они подтягиваются в докер-образ
Минусы:
- дополнительное время на запуск: поправил одну строчку, а все равно ждешь рекомпиляцию + билд контейнера
- сложно запускать тесты, особенно если тебе нужны не все, а выборочные пакеты/тест-кейсы
- невероятно сложно прокидывать дебаггер в контейнер. по сути тебе нужен будет отдельный докерфайл для дебаггинга, а это практически нивелирует перечисленные выше плюсы.
Поэтому я пользуюсь таким подходом:
- пока активно пишу код, запускаю локальный компилятор, локальные тесты
- когда закончил, и версия прилы более менее стабильная, перед тем как отдавать ПР запускаю всю кухню в compose чтобы финально проверить, потыкать апишку руками, запустить тесты в контейнере, и т.д.
-
Q: Имеет ли смысл на данном этапе разработки добавлять тесты? И если да, то для чего их конкретно уже можно написать? Для чего обычно пишутся тесты в проектах на голанг: только для доменной логики или также для транспортного уровня и других пакетов?
A: Да, если добавишь тесты - будет очень круто. На данном этапе можно проверить что http-server стартует корректно, и отдает статус
200по какой-нибудь ручке/healthcheck. Это, кстати, актуальная тема с реальной работы, девопсы очень часто просят выставить такой эндпоинт, так как он активно юзается оркестраторами чтобы мониторить состояние контейнера.Тесты пишутся для всего что важно проверить. Конечно же это в первую очередь бизнес-логика. Транспорт, если ты имеешь ввиду работу протоколов мы не тестируем. Мы полагаемся на то, что гугл написали свою либу для протобаффов корректно, и она работает))
Но в интеграционных тестах часто создаем тестовые клиенты (http, grpc), чтобы проверить что наша апишка работает корректно, а нужные компоненты вызываются с нужными параметрами. Все зависит от масштаба, который используем в тестировании.
Доработки после ревью:
- добавлен линтер golangci-lint с файлом конфигурации
- добавлен pre-commit git hook для исправления ошибок линтинга и добавления изменений в коммит
- добавлен makefile target для инициализации pre-commit hook
- добавлен makefile targets для линтинга и исправления ошибок линтинга
- использовал
viperдля конфигурации приложения вместоgodotenvиcaarlos0/env - добавил healthcheck ручку для проверки состояния приложения
- обновил Dockerfile для сборки и запуска приложения на prod
- добавил Dockerfile image в docker-compose.yml
- добавил Makefile target для запуска приложения для локальной разработки и для продакшена с использованием docker-compose