calendar

module
v0.0.0-...-207d3b7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2024 License: MIT

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
  1. Q: Когда следует использовать оборачивание ошибок при помощи директивы %w, а когда нет? Правильно ли я использовал эту директиву в коде?

    A: Директива %w используется для оборачивания ошибок, когда нужно сохранить информацию о первопричине ошибки.

    Так как в Go ошибка, это всего лишь интерфейс возвращающий строку, она обычно не несет никакого контекста о месте, где она возникла (в этом разница с привычными Exceptions). Поэтому мы сами добавляем нужный нам контекст о возникновении с помощью враппинга. Его можно и нужно делать везде, где важно показать в каком месте и при какой операции возникла ошибка. Не нужно делать там, где мы используем общие стандартизованные ошибки (см. Sentinel Errors).

    В целом ты везде использовал %w правильно. Подсвечу лишь, что в некоторых компаниях принято использовать errors.Wrap для той же операции.

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

  2. Q: Нужно/можно ли использовать кастомные ошибки на текущем этапе разработки? В каких случаях это будет уместно?

    A: Если речь о создании своего типа, который будет имплементировать интерфейс Error, то я не вижу смысла делать это сейчас. Обычно это делают там, где не хватает стандартного функционала ошибок, тогда их делают в виде структур, добавляют метрики, трейсы, вкладывают одну в другую и т.д. Зачастую те, кто пытается играться в ошибки-структуры просто не отвыкли от классических исключений и пытаются затащить этот механизм в Golang (а зря).

    Если же речь про вынос переиспользуемых ошибок в Sentinel Errors, то да, это вполне можно делать уже сейчас, на твоё усмотрение. Я только за.

  3. Q: Почему в бойлерплейте проекта контекст для сервера передается в виде параметра closer функции stopHTTPServer в пакете application? Выглядит так, что он вряд ли будет часто меняться и можно его переместить в пакет server.

    А: Там по сути используется обыкновенное замыкание. Контекст передается не для сервера, а для функции, закрывающей соединение с сервером. Эту функцию мы вызываем в application, объявляя и вызывая анонимную функцию на лету (в JS это называется IIFE - immediately invoked function expression). Из этой анонимной функции возвращается новый контекст, созданный специально для завершения работы сервера с отдельным таймаутом. Обычно это делается, когда мы хотим дать время на закрытие ресурсов. В этом случае мы не используем корневой контекст и создаём новый.

    Где именно создавать новый контекст большой разницы нет, но я догадываюсь почему так сделал Виталий (автор бойлерплейта). Потому что обычно мы хотим управлять контекстом на как можно более верхнем уровне. В main.go обычно создается самый главный (глобальный) контекст который определяет жизненный цикл всего приложения, а в application в данном случае создаем побочный контекст только для одной задачи.

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

  4. Q: Контекст - пункт задания: Используется логгер и он настраивается из конфига. Вопрос: какие настройки логгера следует выносить в конфиг? По какому принципу выносить?

    A: Тут нет никаких правил, но из опыта и здравого смысла могу назвать:

    1. Те, которые меняются в зависимости от среды выполнения. Пример: на локальной машине удобнее читать обычные строки в stdout, в то время как для нормального сбора логов и метрик в продакшне просто необходимо использовать json-форматированные логи, т.к. это де-факто стандарт индустрии. Следовательно -> можно вынести в конфиг тип форматирования.

    2. Те, которые меняются часто. Пример: уровень логгирования (хотим подебажить - врубаем DEBUG, в штатном режиме обычно работает INFO или WARN).

    3. Те, которые сам считаешь нужным вынести, чтобы не создавать лишние коммиты в будущем, когда захочешь поменять что-то. Пример: я задавал формат даты-времени через конфиг, не то чтобы прям часто менял, просто по-приколу :)

    В целом логгер не самая конфигурируемая вещь. Обычно настроил один раз и забыл)

  5. Q: Правильно ли реализован и использован логгер? Рекомендации по улучшению?

    A: Вполне нормально. Если захочешь, можешь добавить настройку форматирования логов в зависимости от переменной в конфиге.

  6. 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, а если задана, то приоритет у неё. Она буквально перезаписывает это же значение из конфига.

  7. Q: На текущий момент запуск приложения golang не докеризируется и приложение запускается напрямую на хосте. Это распространенный подход для локальной среды разработки или лучше добавить использование Dockerfile в docker-compose.yml? Или же Dockerfile здесь нужен только для запуска в продакшен? Хотелось бы узнать про это подробнее. Какие есть практики касательно локальной разработки и докера? Можно ли разрабатывать полностью в докере? Плюсы и минусы?

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

    • Её можно было запустить в другой среде, даже без установки golang-компилятора.
    • Убедиться что все правильно билдится и приложение нормально взаимодействует с другими компонентами внутри докера.
    • Попрактиковаться с докером)) На работе без него никуда в 2k24.

    Локально полностью в докере разрабатывать можно, но это проще делать с интерпретируемыми языками типа python, js, так как там при разработке меняются только файлы с исходным кодом. В случае же Go и других компилируемых языков, исходный код как раз обычно не используется в результирующем контейнере, а используется скомпилированный исполняемый файл. А значит при каждом рестарте после правок у тебя будет заново происходить не только компиляция прилы, но и билд контейнера.

    Плюсы:

    • предсказуемое окружение, если работает в докере -> работает везде (на 99.9%)
    • не зависишь от установленной версии Golang, можешь вообще его не устанавливать)
    • не скачиваешь зависимости локально, они подтягиваются в докер-образ

    Минусы:

    • дополнительное время на запуск: поправил одну строчку, а все равно ждешь рекомпиляцию + билд контейнера
    • сложно запускать тесты, особенно если тебе нужны не все, а выборочные пакеты/тест-кейсы
    • невероятно сложно прокидывать дебаггер в контейнер. по сути тебе нужен будет отдельный докерфайл для дебаггинга, а это практически нивелирует перечисленные выше плюсы.

    Поэтому я пользуюсь таким подходом:

    • пока активно пишу код, запускаю локальный компилятор, локальные тесты
    • когда закончил, и версия прилы более менее стабильная, перед тем как отдавать ПР запускаю всю кухню в compose чтобы финально проверить, потыкать апишку руками, запустить тесты в контейнере, и т.д.
  8. Q: Имеет ли смысл на данном этапе разработки добавлять тесты? И если да, то для чего их конкретно уже можно написать? Для чего обычно пишутся тесты в проектах на голанг: только для доменной логики или также для транспортного уровня и других пакетов?

    A: Да, если добавишь тесты - будет очень круто. На данном этапе можно проверить что http-server стартует корректно, и отдает статус 200 по какой-нибудь ручке /healthcheck. Это, кстати, актуальная тема с реальной работы, девопсы очень часто просят выставить такой эндпоинт, так как он активно юзается оркестраторами чтобы мониторить состояние контейнера.

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

    Но в интеграционных тестах часто создаем тестовые клиенты (http, grpc), чтобы проверить что наша апишка работает корректно, а нужные компоненты вызываются с нужными параметрами. Все зависит от масштаба, который используем в тестировании.

Доработки после ревью:
  1. добавлен линтер golangci-lint с файлом конфигурации
  2. добавлен pre-commit git hook для исправления ошибок линтинга и добавления изменений в коммит
  3. добавлен makefile target для инициализации pre-commit hook
  4. добавлен makefile targets для линтинга и исправления ошибок линтинга
  5. использовал viper для конфигурации приложения вместо godotenv и caarlos0/env
  6. добавил healthcheck ручку для проверки состояния приложения
  7. обновил Dockerfile для сборки и запуска приложения на prod
  8. добавил Dockerfile image в docker-compose.yml
  9. добавил Makefile target для запуска приложения для локальной разработки и для продакшена с использованием docker-compose

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL