📦 pkg directory considered harmful
🚫 Почему не стоит держать код приложения вне директории internal
Чтобы понять, в чём проблема, нужно разобраться с понятием «публичный интерфейс». Я уже писал статью, где постарался понятно объяснить, что это такое и почему термин «интерфейс» часто вызывает путаницу (из-за перегруженности термина). С ней можно ознакомитьс
я тут. В этом тексте речь идёт именно о публичном интерфейсе, а не о типе данных interface!
При проектировании публичных интерфейсов важно понимать:
🔒их нельзя менять без необходимости — иначе сломаются клиенты, которые от них зависят. То есть нужно сохранять обратную совместимость. По этой причине приложения версионируют (с
м. https://semver.org/lang/ru/) и ломают обратную совместимость только в мажорных релизах (MAJOR.minor.patch), которые выходят намного реже, чем minor и patch. Чтобы не приходилось часто выпускать мажорные релизы, интерфейс нужно тщательно продумывать и делать минимальным (минимально необходимым).
📌 Например, в HTTP API это означает, что не стоит создавать несколько эндпоинтов для похожих действий. Вместо /users/active и /users/inactive лучше сделать один — /users?status=active|inactive.
Публичным интерфейсом может быть не только HTTP API, но и код, от которого зависят другие приложения. И этот интерфейс также необходимо делать минимально возможным, потому что всё, что мы однажды сделали публичным, — обязаны поддерживать.
🚧 Защита кода с помощью internal
Чтобы исключить возможность того, что другие приложения будут зависеть от нашего кода, нужно запретить этот код импортировать. Для этих целей в Go 1.4 появилась директория internal, которая запрещает импортировать то, что внутри нее любому коду на уровень выше (в том числе из другого репозитория).
📖 Подробнее про interna
l - в документации.
🤡 Но часто этой директории придают другие значения — например, «в internal кладём бизнес-логику, а в pkg — библиотеки». Это ошибочное представление, которое возникло из-за исторических причин, а сейчас распространено т.к. бездумно копируется. На самом деле любые трактовки internal, отличные от официальной, понятны только тем, кто их придумал. Если использовать internal по назначению, то в нее попадает код, который мы запрещаем импортировать, а вне internal остается код, который разрешаем импортировать, а никакой не "бизнес код", не "код, который должен стать библиотекой в дальнейшем" и т.д. Если есть код, который планируется в будущем сделать библиотекой, но сейчас еще не пришло время для этого, то можно его поместить внутри internal, например, создав уже там директорию pkg.
Часто в pkg хранят "клиенты" от http или gRPC API приложения, и хоть это удобно, но архитектурно это уязвимо. Внутри этих клиентов код часто использует пакеты самого приложения (со структурами например), поэтому импортируя этот клиент, неявно импортируются и другие пакеты. Правильнее хранить клиенты в отдельном репозитории и использовать "свои" структуры (определенные в репозитории клиента). А не свои и не получится т.к. в основном приложении они будут лежать в internal!
🔗 Полезные сс
ылки:
- https://eli.thegreenplace.net/2019/simple-go-project-layout-with-modules/ (последний а
бзац)
- https://medium.com/golang-learn/go-project-layout-e5213cd
cfaa2
- https://go.dev/doc/modules/layout — хорошая статья от команды Go про структуру проектов. Я считаю, что она должна быть основой, которую надо читать на эту тему, а не распиаренный golang-standards/project-layout (который, вопреки названию, не является стандартом).
🗂 Какую структуру использовать
В большинстве случаев Go-проект содержит всего две директории с го кодом:
- cmd с main.go
- internal - со всем остальным кодом
Как организовать содержимое внутри internal — зависит от многого. Я считаю идеи чистой архитектуры полезными и делаю свой репозиторий, где стараюсь следовать этим прин
ципам:
📎 https://github.com/rostislaved/go-clean-architecture