Этот пост также доступен на моем собственном сайте здесь.

Го - потрясающий язык. Это просто, легко рассуждать и сразу же предоставляет множество инструментов. Однако, когда я начал работать с Go, я изо всех сил пытался понять, как структурировать свои приложения таким образом, чтобы не использовать «корпоративные» подходы. Это мой подход к структурированию приложений Golang, которые изначально просты, но обладают гибкостью для роста, и то, что я хотел бы, когда начинал с Go.

Отказ от ответственности

Эта статья основана на материалах следующих людей (ссылка на их Twitter): Мэт Райер, Кэт Зиен, Джон Калхун и Роберт К. Мартин.

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

Если вы просто хотите увидеть код проекта этой статьи, загляните в репозиторий Github.

Проект и инструменты

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

Теперь я предполагаю, что знаком с Golang и программированием в целом, поскольку это практический пример того, как структурировать приложение Go. Мы будем использовать следующее:

  • Джин Веб Фреймворк
  • Go версии 1.15+
  • Postgresql
  • Голанг-миграция
  • Ваша любимая IDE (может быть код Goland или VS - я настоятельно рекомендую Goland, это потрясающе)

Элементы надежной и масштабируемой структуры

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

  • Тестируемость
  • Читаемость
  • Адаптивность

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

Если начать как можно проще, у вас будет преимущество в том, что вы сможете быстро перерабатывать новые идеи, с самого начала переходя к фактическому решению проблемы. Большинству разработчиков не нужно думать о разработке на основе предметной области при запуске новых проектов, и в большинстве случаев это, вероятно, будет пустой тратой времени (если, конечно, вы не работаете в Big Co., но тогда зачем вам читать статья для начинающих разработчиков Go).

Учебное пособие разделено на три части:

  • Структура приложения
  • Реализация пользовательского сервиса
  • Внедрение службы весов TDD

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

Структура приложения

Хорошо, хватит разговоров, поехали. Создайте новую папку с именем weight-tracker в любом месте, где вы хотите хранить свои проекты, и выполните следующую команду внутри папки:

go mod init weight-tracker

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

weight-tracker
- cmd
  - server
    main.go
- pkg
  - api
    user.go
    weight.go
  - app
    server.go
    handlers.go
    routes.go
  - repository
    storage.go

Идите вперед и создайте указанную выше структуру в назначенной папке.

Большинство проектов go, похоже, следуют соглашению о наличии директорий cmd и pkg. cmd будет точкой входа в программу и даст вам гибкость для взаимодействия с программой несколькими способами. Каталог pkg будет содержать все остальное: маршруты, взаимодействия с базой данных, службы и т. Д.

Мы будем работать с четырьмя пакетами:

  • главный
  • api
  • приложение
  • хранилище

Пакет main не требует пояснений, если вы когда-либо ранее занимались программированием на golang. Все наши службы, то есть службы user и weight, входят в пакет api вместе с файлом definitions, который будет содержать все наши структуры (мы создадим его позже). В пакете app у нас будут наши server, handlers и routes.. Наконец, у нас есть repository, который будет содержать весь наш код, связанный с операциями с базой данных.

Откройте main.go и добавьте следующее:

У вас должно быть довольно много ошибок, отображаемых в вашей среде IDE, мы исправим это в ближайшее время. Но сначала позвольте мне объяснить, что здесь происходит. В нашем func main() мы вызываем метод с именем run, который возвращает ошибку (или ноль, если ошибки нет). Если run должен вернуть ошибку, наша программа завершает работу и выдает нам сообщение об ошибке. Настройка нашей main функции таким образом позволяет нам протестировать ее и, тем самым, следовать элементу надежной структуры сервиса - тестируемости. Этот способ настройки основной функции был предложен Мэтом Райером, который подробно рассказал об этом в своем сообщении в блоге.

Следующие две темы, которые нам нужно обсудить, - это Чистая архитектура и Внедрение зависимостей. Возможно, вы слышали о чистой архитектуре под другим названием, поскольку несколько человек писали о ней примерно в то же время. Большая часть моего вдохновения исходит от Роберта К. Мартина, поэтому я буду ссылаться именно на него.

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

Цитата из статьи Роберта Мартина помогает определить это правило:

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

Грубо говоря, внутренние слои не должны знать о внешних слоях. Это позволяет нам практически изменять используемую базу данных. Скажем, мы переходим с PostgreSQL на MySQL или открываем наш API через gRPC вместо HTTP. Скорее всего, вы никогда этого не сделаете, но это говорит об адаптивности этой концепции.

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

Лучше всего я учусь, глядя на реальный код, поэтому давайте начнем с добавления недостающего кода, который сделает код в main.go более понятным. Откройте storage.go и добавьте следующее:

Это позволяет нам создавать новый компонент хранилища где и когда мы хотим, пока он получает действительный аргумент db типа *sql.DB.

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

Теперь давайте настроим базовую структуру одной из наших служб, user.go. Это должно дать вам представление о том, как пакет API будет структурировать службы. Вам придется повторить это и для службы weight.go. Просто скопируйте и вставьте содержимое user.go и измените название. Откройте user.go и добавьте следующее:

Обратите внимание, что у нашей пользовательской службы есть зависимость от репозитория. Мы определим здесь методы репозитория (то есть операции с базой данных) позже, но важная часть состоит в том, что мы определяем только те методы, которые нам нужны. Поскольку нас не интересует реализация этих методов, а только их поведение, мы можем написать фиктивные функции, которые подходят для данного тестового примера.

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

Откройте server.go и добавьте следующее:

Затем откройте routes.go и добавьте следующее:

Мы собираемся воспользоваться функциональностью группы gin, чтобы мы могли легко сгруппировать наши конечные точки по ресурсам, которые они намереваются обслуживать. Затем давайте добавим обработчик, чтобы мы действительно могли звонить в конечную точку состояния и проверять, работает ли наше приложение. Откройте handlers.go:

На данный момент нам нужно синхронизировать только несколько зависимостей: Gin Web Framework и драйвер для PostgreSQL. Идите вперед и введите в свой терминал следующее: go get github.com/gin-contrib/cors , go get github.com/gin-gonic/ginand go get github.com/lib/pg.

Теперь все должно быть готово, поэтому войдите в свой терминал и напишите: go run cmd/server/main.go, посетите http://localhost:8080/v1/api/status, и вы должны получить сообщение следующего содержания:

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

Реализация пользовательского сервиса

На этом этапе мы готовы приступить к созданию сути приложения. Для этого нам, вероятно, потребуется создать базу данных с таблицами и отношениями. Для этого воспользуемся библиотекой golang-migrate. Мне лично нравится эта библиотека, поскольку в ней есть инструмент CLI для добавления миграций, который позволяет мне делать такие вещи, как добавление команд Makefile для создания миграций. Я рекомендую вам взглянуть на документацию библиотеки, это потрясающий проект.

Я не буду рассказывать, как это сделать, так как статья будет слишком длинной. А пока войдите в свой терминал, убедитесь, что вы находитесь в папке repository, и запустите:

git clone https://gist.github.com/ed090d782dc6ebb35e344ff82aafdddf.git

Это клонирует миграции, необходимые для проекта. У вас также должна быть папка с именем ed090d782dc6ebb35e344ff82aafdddf, позволяет изменить ее на миграции, запустив:

mv ed090d782dc6ebb35e344ff82aafdddf migrations

Последнее, что нам нужно, это добавить метод RunMigrations в storage.go:

Чтобы запустить миграцию, откройте main.go и добавьте следующее:

Теперь в вашей базе данных должно быть две таблицы: user и weight. Приступим к написанию реальной бизнес-логики.

Мы хотим, чтобы люди могли создавать учетные записи через наш API. Итак, давайте начнем с определения того, что такое запрос пользователя, создадим файл с именем в папке API definitions.go и добавим следующее:

Мы определяем, как должен выглядеть новый пользовательский запрос. Обратите внимание, что это может отличаться от того, как могла бы выглядеть пользовательская структура, мы определяем нашу структуру, чтобы включать только те данные, которые нам нужны. Затем откройте user.go и следующее до UserService и UserRepository:

Здесь мы определяем метод в нашем UserService с именем New и метод в UserRepository с именем CreateUser. Помните, мы говорили о Правиле зависимости ранее? Это то, что происходит с методом CreateUser на UserRepository, наша служба не знает о фактической реализации метода, типе базы данных и т. Д. Просто существует метод с именем CreateUser, который принимает NewUserRequest и возвращает ошибку. . У этого есть двоякая выгода: мы получаем от нашей IDE некоторое указание на то, что метод отсутствует (откройте main.go и проверьте api.NewUserService) и что ему нужно, и это позволяет нам легко писать модульные тесты. Вы также должны увидеть сообщение об ошибке NewUserServicein user.go, сообщающее нам, что нам не хватает метода. Давайте исправим это, добавим следующее:

Мы делаем некоторые базовые проверки и нормализацию, но этот метод определенно можно улучшить. Нам все еще нужно добавить метод CreateUser, поэтому откройте storage.go и добавьте следующее к методу CreateUser интерфейса Storage:

Обратите внимание, что это устраняет ошибку в main.go, но приводит к новой ошибке в функции NewStorage. Нам нужно реализовать метод, как мы это сделали с UserService. Добавьте это ниже RunMigrations:

Опять же, этот код, вероятно, можно было бы улучшить, но я буду краток.

Теперь все, что осталось сделать, - это открыть это через HTTP, чтобы люди действительно могли начать использовать наш API и создавать свои учетные записи. Откройте handlers.go и добавьте следующее:

Мы собираемся принять запрос с полезной нагрузкой JSON и использовать метод gin ShouldBindJSON для извлечения данных. Давай, попробуй!

Чтобы добраться сюда, потребовалось некоторое время, поэтому позвольте мне показать вам одно из преимуществ, о которых я постоянно говорю: тестируемость.

Создайте файл с именем user_test.go в папке API и добавьте следующее:

Здесь происходит много всего, поэтому давайте рассмотрим их шаг за шагом.

Начнем с создания структуры с именем mockUserRepo. Наш UserService знает, что ему нужен UserRepository с помощью следующего метода: CreateUser. Однако он действительно заботится о фактической реализации указанного метода, что позволяет нам имитировать поведение любым удобным нам способом. В этом случае мы говорим, что когда имя запроса равно «тестовый пользователь уже создан» (плохое имя, я знаю, но, надеюсь, вы поняли), вернуть ошибку, а если нет, просто вернуть nil. Это позволяет нам имитировать поведение нашей базы данных, чтобы она соответствовала различным ситуациям, и проверять, обрабатывает ли наша логика так, как мы этого ожидаем.

Затем мы создаем новую переменную с именем mockRepo типа mockUserRepo. Затем мы создаем mockUserService и передаем ей mockRepo, и мы готовы к работе! Настоящий тест известен как тест, управляемый таблицами. Я не буду вдаваться в подробности об этом, поскольку это выходит за рамки данной статьи, но если вы хотите узнать больше, ознакомьтесь со статьей Дэйва Чейни об этом.

Теперь, когда мы добавляем метод в UserRepository, нам также нужно будет добавить его сюда, в наш mockUserRepo. Мы, конечно, также хотели бы провести некоторый интеграционный тест, но в этой статье я действительно хотел показать, как выполнять модульные тесты, поскольку они дешевы и просты в написании.

Запустите go test ./... из корневого каталога, и все тесты должны пройти.

Внедрение службы весов TDD

Наше приложение сейчас не стоит многого. Мы можем только создать пользователя, но не можем отслеживать свой вес или рассчитывать необходимое нам количество калорий. Давай изменим это!

Наше приложение сейчас не стоит многого. Мы можем только создать пользователя, но не можем отслеживать свой вес или подсчитывать необходимое нам количество калорий. Давайте изменим это!
Я большой поклонник разработки через тестирование (TDD), и то, как структурировано это приложение, делает его действительно простым в использовании. Нам понадобятся три метода в нашем сервисе весов: New, CalculateBMR и DailyIntake и два в нашем репозитории: CreateWeightEntry и GetUser. Откройте weight.go и добавьте в WeightService и WeightRepository следующее:

Затем нам нужно добавить три структуры в наш файл определений: ´NewWeightRequest, Weight и User. Откройте weight.go и добавьте следующее:

Теперь вы увидите ошибку NewWeightService об отсутствующих методах. Мы пока не хотим писать реальную реализацию, так как делаем TDD, поэтому пока просто добавьте это ниже NewWeightService:

Откройте main.go, и вы увидите, что у нас также есть некоторые недостающие методы хранения, переданные в api.NewWeightService. Откройте storage.go и добавьте эти:

Теперь давайте добавим тесты, которые нам понадобятся, создадим файл с именемweight_test.go в папке API и добавим следующее:

Фактическое содержание тестов не так важно, как тот факт, что мы можем быстро писать тесты и имитировать внешние зависимости, такие как методы взаимодействия с базой данных.

Запустите go test ./... из корневого каталога, и вы получите много неудачных тестов. Мы собираемся исправить это, реализовав логику этих методов, начиная с трех в нашем WeightService. Добавьте это ниже NewWeightService:

С добавлением этих методов все тесты должны пройти.

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

Мы не сделали это в истинном стиле TDD, однако он должен снова нарисовать картину того, как мы можем структурировать приложения Go и разрабатывать их с использованием TDD. Не стесняйтесь повторно реализовать этот раздел и сделать это в истинном стиле TDD: создать один тест, пройти его, создать следующий и так далее.

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

Конец

Надеюсь, это руководство дало вам некоторое представление о том, как структурировать приложения Golang. Я знаю, что это было долго, но, надеюсь, вы не потратили зря время. Во второй части я покажу, как добавить команды Makefile, тесты интеграции и легко развернуть приложение. Если у вас есть какие-либо вопросы или замечания, не стесняйтесь обращаться к нам.

Мой сайт, твиттер и гитхаб.

Ресурсы

Project GitHub
Чистая архитектура
Golang-migrate
Табличные тесты