Подробное руководство по организации пакетов в Go

Подробное руководство по организации пакетов в 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 инициализирует и связывает все вместе.

:::информация Также опубликовано здесь.

:::


Оригинал