
Подробное руководство по организации пакетов в Go
16 февраля 2023 г.Неизбежно каждый разработчик Go задает следующий вопрос:
<цитата>Как упорядочить код?
Есть много статей и подходов, и хотя некоторые из них хорошо работают для одних, они могут не сработать для других. В Go нет официальных соглашений и предпочтений относительно того, как вы должны структурировать свои пакеты и где должны находиться ваши ресурсы, не связанные с кодом.
Однако, в отличие от других языков, Go не поддерживает циклический импорт пакетов. Таким образом, проекты требуют дополнительного планирования при группировании кода в пакеты, чтобы гарантировать, что зависимости не будут импортировать друг друга.
Цель этой статьи не в том, чтобы установить строгие правила, а в том, чтобы пропагандировать создание надежных ментальных моделей для рассуждений. о ваших проблемных областях и о том, как представить их в макетах вашего проекта.
Типы макетов проекта
Нет идеальных подходов, но некоторые из них получили широкое распространение. Пожалуйста, проверьте следующие ресурсы:
* статья Бена Джонсона на Стандартный макет упаковки. * Макет проекта Golang Standards. * прекрасный доклад, презентацию и примеры кода Кэт Зиен можно найти здесь.
Теперь мы собираемся изучить, какие варианты доступны для структурирования ваших приложений, и провести различие между хорошими и плохими практиками.
Плоская структура
- Вместо того, чтобы тратить время на то, чтобы понять, как разбить код на пакеты, приложение с плоской структурой просто поместит все файлы
.go
в один пакет. - Поначалу это звучит ужасно, потому что мы не используем пакеты, которые разделяют задачи и упрощают быстрый переход к нужным исходным файлам.
Рекомендации
При использовании плоской структуры все равно следует стараться придерживаться передовых методов написания кода. Вот несколько полезных советов:
* Вы хотите разделить разные части приложения с помощью разных файлов .go
:
restaurant-app/
customer.go
data.go
handlers.go
main.go
reservation.go
server.go
storage.go
storage_json.go
storage_mem.go
* Глобальные переменные по-прежнему могут создавать проблемы, поэтому вам следует подумать об использовании типов с методами, чтобы исключить их из кода:
package main
import (
"net/http"
"some"
"someapi"
)
type Server struct {
apiClient *someapi.Client
router *some.Router
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
* Ваша функция main()
по-прежнему должна быть лишена большей части логики, кроме настройки приложения.
* Возможное улучшение плоской структуры — снова поместить весь ваш код в один пакет, но отделить пакет main
, в котором вы определяете точку входа приложения. Это позволит вам использовать общий шаблон подкаталога cmd
:
restaurant-app/
cmd/
web/
# package main
main.go
cli/
# package main
main.go
# package restaurantapp
server.go
customer_handler.go
reservation_handler.go
customer_store.go
* Этот макет проекта обычно подходит для небольших библиотек или инструментов CLI и очень простых проектов.
Антишаблон: группировка по функциям (многоуровневая архитектура)
Шаблоны многоуровневой архитектуры представляют собой n-уровневые шаблоны, в которых компоненты организованы в слои.
Это традиционный метод разработки большинства программ, который должен быть самонезависимым, т. е. все компоненты взаимосвязаны, но НЕ зависят друг от друга.
Мы все слышали о знаменитой трехуровневой архитектуре MVC (модель-представление-контроллер), в которой мы разделяем наше приложение на следующие 3 отдельных уровня:
— Презентация/пользовательский интерфейс (Просмотр)
— Бизнес-логика (Контроллер)
— Хранилище/Внешние зависимости (Модель)
Эта архитектура, переведенная в макет нашего примерного проекта, будет выглядеть так:
restaurant-app/
# package main
data.go
handlers/
# package handlers
customers.go
reservations.go
# package main
main.go
models/
# package models
customer.go
reservation.go
storage.go
storage/
# package storage
json.go
memory.go
...
* Этот тип макета вообще НЕ должен компилироваться из-за циклических зависимостей.
* Пакет storage
использует пакет models
для получения определений для Customer
и Reservation
, а также models
использует пакет storage
для обращения к базе данных.
* Еще одним недостатком этой структуры является то, что она не указывает нам, что делает приложение (по крайней мере, не больше, чем плоская структура).
* Этот тип макета настоятельно НЕ рекомендуется при написании приложений на Go, поэтому старайтесь избегать его.
Антишаблон: группировка по модулю
Группировка по модулям предлагает нам небольшое улучшение по сравнению с многоуровневым подходом:
restaurant-app/
customers/
# package customers
customer.go
handler.go
# package main
main.go
reservations/
# package reservations
reservation.go
handler.go
storage/
# package storage
data.go
json.go
memory.go
storage.go
...
* Теперь наше приложение структурировано логично, но это, пожалуй, единственное преимущество такого подхода.
* По-прежнему трудно решить, например, следует ли reservation
перейти в пакет customers
, потому что это резервирования клиента, или они подходят для собственного пакета.
* Именование стало хуже, потому что теперь у нас есть reservations.Reservation
и customers.Customer
, что приводит к заиканию.
* Хуже всего то, что снова может возникнуть циклическая зависимость, если пакет reservations
должен ссылаться на пакет customers
и наоборот.
Группировка по контексту (дизайн, ориентированный на домен)
Такой подход к приложениям называется Domain Driven Design (DDD).
По сути, он помогает вам подумать о предметной области, с которой вы имеете дело, и обо всей бизнес-логике, даже не написав ни одной строки кода.
Необходимо определить три основных компонента:
— Ограниченные контексты
— Модели в каждом контексте
— Повсеместный язык
Ограниченные контексты
Ограниченный контекст — это причудливый термин, определяющий ограничения ваших моделей. Примером может служить, например, сущность Пользователь
, к которой могут быть прикреплены разные свойства в зависимости от контекста:
* Пользователь
в контексте отдела продаж может иметь такие свойства, как leadTime
, costOfAquisition
и т. д.
* Пользователь
в контексте службы поддержки может иметь такие свойства, как responseTime
, numberOfTicketsHandled
и т. д.
* Это демонстрирует, что Пользователь
означает разные вещи для разных людей, и это значение сильно зависит от контекста.
Ограниченный контекст также помогает решить, что должно оставаться неизменным в рамках определенных границ, а что может меняться независимо.
* Если позже мы решим добавить новое свойство в User
из контекста отдела продаж, это не повлияет на модель User
в контексте поддержки клиентов.
Вездесущий язык
Вездесущий язык — это термин, используемый в Дизайне, ориентированном на предметную область, для практики создания общего строгого языка между разработчиками и пользователями.
Этот язык основан на Модели предметной области, используемой в программном обеспечении, и он развивается до такой степени, что позволяет выражать сложные идеи путем объединения простых элементов Модели предметной области.
Классификация строительных блоков
Основываясь на методологии DDD, мы теперь начнем рассуждать о нашей области, создавая ее стандартные блоки.
Если мы возьмем наш пример Система резервирования в ресторане, у нас будут следующие элементы:
- Контекст: резервирование.
- Язык: резервирование, клиент, хранение,…
- Объекты: резервирование, клиент,…
- Объекты-ценности: ресторан, хост и т. д.
- Агрегаты: BookedReservation
- Сервис: Список/список резервирования, Добавление/добавление резервирования, Добавление/добавление клиентов, Список/список клиентов,…
- События: ReservationAdded, CustomerAdded, ReservationAlreadyExists, ReservationNotFound,…
- Репозиторий: ReservationRepository, CustomerRepository, …
Теперь, после определения этих блоков, мы можем преобразовать их в макет нашего проекта:
restaurant-app/
adding/
endpoint.go
service.go
customers/
customer.go
sample_customers.go
listing/
endpoint.go
service.go
main.go
reservations/
reservation.go
sample_reservations.go
storage/
json.go
memory.go
type.go
Главное преимущество здесь в том, что наши пакеты теперь сообщают о том, что они ПОСТАВЛЯЮТ, а не о том, что они СОДЕРЖАТ.
Это позволяет избежать циклических зависимостей, потому что:
добавление
илистинг
взаимодействуют схранилищем
.storage
извлекается изклиентов
ибронирований
.- Пакеты моделей, такие как
reservations
иcustomers
, не заботятся охранилище
напрямую.
Группировка по контексту (дизайн, ориентированный на предметную область + шестиугольная архитектура)
На данный момент нам удалось структурировать наше приложение в соответствии с DDD, устранить циклические зависимости и сделать интуитивно понятным то, что делает каждый пакет, только взглянув на него. в каталоге и именах файлов.
Однако у нас все еще есть некоторые проблемы:
* Как мы можем запустить версию нашего приложения, содержащую демонстрационные данные*?*?
* В нашей текущей версии образцы данных связаны с точкой входа приложения в main.go
и у нас есть только один main.go
.
* У нас НЕТ возможности запускать тестовую версию данных отдельно от основной версии приложения.
* Возможно, нам нужна чистая CLI-версия приложения где вместо добавления резервирований через HTTP-запросы мы хотим, чтобы командная строка подсказывать нам для каждого резервируемого объекта?
Шестиугольная архитектура
В этом типе архитектуры выделяются части системы, формирующие ваш основной домен, а все внешние зависимости – это просто детали реализации.
Рис. 1: Диаграмма уровней шестиугольной архитектуры
Внешними зависимостями могут быть базы данных, внешние API, почтовые клиенты, облачные сервисы и т. д. — все, с чем взаимодействует ваше приложение.
Проблема, которую это решает, заключается в том, что вы можете изменить одну часть приложения, не затрагивая остальную часть, например, поменять местами базы данных или транспортные протоколы (с HTTP на gRPC).
Это никоим образом не похоже на модель MVC (многоуровневая), потому что:
- MVC имеет тенденцию рассматривать входные и выходные данные сверху вниз (ввод -> основная логика -> вывод).
Рис. 2: Направление зависимостей многоуровневой архитектуры
* Hex обрабатывает входные и выходные данные на одном уровне. Ему все равно, является ли что-то входом или выходом, это просто внешний интерфейс.
Рис. 3. Направление инверсии зависимостей
* Ключевое правило модели hex заключается в том, что зависимости указывают только ВНУТРЬ (только внешние слои зависят от внутренних слоев, а не наоборот) . Это называется принципом инверсии зависимостей. Прочтите эту превосходную статью Мартина Фаулера, чтобы узнать больше.
Рекомендации
При таком подходе структура нашего проекта может выглядеть так:
restaurant-app/
cmd/
# HTTP server
restaurant-server/
main.go
# CLI app
restaurant-cli/
main.go
# HTTP server with seeded data
restaurant-sample-data/
main.go
sample_reservation.go
sample_customers.go
pkg/
adding/
reservation.go
endpoint.go
service.go
listing/
customer.go
reservation.go
endpoint.go
service.go
transport/
http/
server.go
main.go
storage/
json/
customer.go
repository.go
reservation.go
memory/
customer.go
repository.go
reservation.go
Чтобы решить проблему с двоичными файлами нескольких версий приложения, мы используем шаблон подкаталога cmd
, который мы упомянули как улучшение макета плоской структуры.< /p>
Теперь мы можем создавать 3 разных двоичных файла, используемых для разных целей:
restaurant-server
— основная версия приложения, развертывающая HTTP-сервер.restaurant-cli
— версия CLI с удаленным транспортным уровнем, предлагающая интерфейс CLI для взаимодействия.restaurant-sample-data
– образец заполненной версии данных, используемый в основном для тестирования.
Мы представляем пакет pkg
, который отделяет наш код Go от двоичных файлов cmd
и ресурсов, не связанных с кодом, например. Скрипты БД, конфигурации, документация и т. д., которые должны находиться на том же уровне в корневом каталоге проекта.
ПРИМЕЧАНИЕ
Использование каталогов cmd
и pkg
стало своего рода тенденцией в сообществе Go. . Это ни в коем случае не стандарт, а хорошая рекомендация, которую следует учитывать.
* В соответствии с DDD мы сохраняем пакеты adding
и listing
, которые представляют наш основной домен.
* Мы удаляем пакеты reservations
и customers
и вместо этого вводим модели в каждый из основных пакетов предметной области, например, adding.Reservation
, adding. .Customer
, listing.Reservation
и т. д.
* Преимущество здесь в том, что у нас есть отдельные представления для каждой модели в соответствии с ограниченным контекстом (добавление
или листинг
). Это позволяет раздельно модифицировать модель и избежать циклических зависимостей.
* Мы представляем пакет transport
, который содержит все реализации транспортных протоколов, например. HTTP или, возможно, gRPC в соответствующих подпакетах.
* Пакет storage
— это еще один ограниченный контекст, в котором представлены представления модели на уровне хранилища и вложенные пакеты для реализации хранилища, например. json
, память
и т. д.
* Опять же, main.go
связывает все вместе и не должен содержать никакой логики, требующей тестирования.
Заключение
К сожалению, единого правильного ответа не существует, но, по крайней мере, мы наметили несколько примеров проблемных областей, как рассуждать о них и как перевести эти рассуждения в структуру пакета Go. Большая свобода приходит с большой ответственностью! Используйте следующие рекомендации с умом:
* «Делайте все как можно проще, но не проще». — Альберт Эйнштейн.
* Сохраняйте последовательность.
* Плоский и простой подходит для небольших проектов.
* Избегайте глобальной области видимости и init()
.
* Отдельный зависимый код в отдельный пакет.
* Два каталога верхнего уровня: — cmd
(для ваших двоичных файлов) — pkg
(для ваших пакетов)
* Все остальные файлы проекта (скрипты БД, фикстуры, ресурсы, документы, конфигурации Docker и т. д.) – должны находиться в корневом каталоге вашего проекта.
* Группировка по контексту, а не по общей функциональности. Попробуйте DDD/Hex.
* Пакет main
инициализирует и связывает все вместе.
:::информация Также опубликовано здесь.
:::
Оригинал