Чтобы поделиться кодом, нужно создать библиотеку и разместить её в самостоятельном репозитории. Но иногда возникает необходимость хранить библиотеку вместе с сервисом, который её использует. Среди Go-разработчиков существует мнение, что экспортируемые библиотеки стоит хранить в директории pkg.
История этой директории берёт начало со времён ранних релизов Go, когда модули стандартной библиотеки находились в $GOROOT/src/pkg. Впоследствии директория pkg была удалена, но многие проекты, такие как Kubernetes, повторили у себя данную файловую структуру. С тех пор pkg закрепилась в файловой структуре Go-проектов.
Когда же лучше хранить библиотечный код в одном репозитории с сервисом?
-
При разработке в open source, если не хочется плодить множество отдельных репозиториев.
-
В процессе дробления монолита на микросервисы. Монолит экспортирует часть своего кода, но при этом остаётся его владельцем. Это позволяет создавать в монолите pull request, который модифицирует одновременно и основной код монолита, и код библиотеки.
-
При шеринге своим API. Например, можно хранить в pkg сгенерированный на основе .proto-файла клиент сервиса. Это позволит вашим клиентам удобнее обращаться к вам по gRPC-протоколу.
Как оказалось, при экспорте библиотеки из сервиса может возникнуть множество нюансов. В этой статье мы разберём, как сделать внешнюю библиотеку максимально удобной как для сервиса, который её экспортирует, так и для импортёров.
Я не буду подробно рассматривать функционал Workspaces, появившийся в Go 1.18. Используя его, удобно работать с несколькими репозиториями одновременно. Это может быть альтернативой предлагаемому в статье подходу: вы выносите библиотеку в отдельный репозиторий и продолжаете работать с ней в вашем сервисе при помощи Workspaces. У такого подхода есть свои плюсы и минусы. Мы же сосредоточимся на работе с несколькими файлами go.mod в рамках одного репозитория.
Дисклеймер: В конце статьи кратко описаны все необходимые шаги. Также вы можете посмотреть готовое решение в моём GitHub-репозитории. А все советы из статьи можно найти в Wiki про Golang-модули.
Наивный вариант
Первое решение, которое приходит на ум, — просто перенести экспортируемый код в папку pkg. Так мы даём понять импортёрам, что делимся данным кодом.
Наш сервис может продолжать пользоваться библиотекой как раньше — она просто переехала в другую папку. Однако при таком решении возникают две проблемы.
1. Лишние зависимости
Сервис, который захочет импортировать библиотеку, будет вынужден импортировать весь наш сервис, со всеми его зависимостями. Это может привести к конфликтам версий. Поддерживать разросшиеся зависимости будет затруднительно. Давайте попробуем проиллюстрировать эту проблему. Создадим новый проект и импортируем в него библиотеку mylib.
> mkdir test_module > cd test_module > go mod init test_module > go get github.com/LopatkinEvgeniy/go-pkg-example@v1.0.0
// test_module/main.go package main import ( "fmt" "github.com/LopatkinEvgeniy/go-pkg-example/pkg/mylib" ) func main() { fmt.Println(mylib.Add(1, 2)) }
> go mod tidy
Теперь заглянем в go.sum. Там мы видим множество зависимостей, например github.com/spf13/cobra. Большинство из этих зависимостей не требуются для работы с mylib.
// go.sum github.com/LopatkinEvgeniy/go-pkg-example v1.0.0 h1:HmUBFee1s+OilGd6MfOfa/hmS8IiAeyX7OcDxDi3c6Y= github.com/LopatkinEvgeniy/go-pkg-example v1.0.0/go.mod h1:n06hzrG2O+vcY3y0r+LyTVHq5cvkpEcsrBe2aUnP/tM= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Проблема в том, что мы импортируем не только библиотеку mylib, но и весь сервис go-pkg-example.
“You wanted a banana but what you got was a gorilla holding the banana and the entire jungle“
Joe Armstrong
2. Общее версионирование
При таком решении версия библиотеки и версия нашего сервиса будут совпадать. Но если сервис, например, повысит версию с 1.0.0 до 2.0.0, то код библиотеки в pkg не изменится. Нужно искать способ версионировать библиотеку и сервис по отдельности.
Отдельный package
Чтобы решить проблемы предыдущего подхода, нужно сделать экспортируемую библиотеку самостоятельным Golang-модулем. Это позволит избавиться от лишних зависимостей для наших клиентов: при импорте библиотеки будут подтягиваться только её собственные зависимости.
> cd pkg/mylib > go mod init github.com/LopatkinEvgeniy/go-pkg-example/pkg/mylib
Эту библиотеку теперь можно версионировать отдельно от сервиса. Для этого создадим git tag, который начинается с пути до библиотеки, а заканчивается её версией.
> git tag pkg/mylib/v1.0.0
Поскольку наш сервис начал экспортировать библиотеку, следует соблюдать принципы семантического версионирования. Если в процессе разработки в библиотеку внесены изменения, нарушающие обратную совместимость API, то нужно увеличить мажорную версию библиотеки.
> git tag pkg/mylib/v2.0.0
Более подробно про семантическое версионирование читайте в спецификации.
Также обратите внимание на то, что в теге между путём до экспортируемой библиотеки и её версией используется разделитель “/”. Из-за этого могут возникнуть проблемы, если путь к вашей библиотеке заканчивается директорией, которая выглядит как версия библиотеки, например “pkg/grpc-api/v1” или “pkg/mylib/v1”. Избегайте такого именования, если используете данный подход.
Теперь наши клиенты могут добавить библиотеку к себе в зависимости, прописав требуемую версию. При этом подтянутся только зависимости библиотеки mylib, а не всего сервиса.
> go get github.com/LopatkinEvgeniy/go-pkg-example/pkg/mylib@v1.0.0
Если вы повторили локально пример из прошлой главы, то вместо данной команды измените ваш go.mod.
module test_module go 1.17 require github.com/LopatkinEvgeniy/go-pkg-example/pkg/mylib v1.0.0
> go mod tidy
Теперь посмотрите на содержимое файла go.sum. Как видите, лишние зависимости были удалены.
// go.sum github.com/LopatkinEvgeniy/go-pkg-example/pkg/mylib v1.0.0 h1:AD3VZ9PaBkQ7DwbRl2NkUy15vMKE/OI6Y/qSNmC4L40= github.com/LopatkinEvgeniy/go-pkg-example/pkg/mylib v1.0.0/go.mod h1:KSTqJV3ZlY5nBKt2wkeps0ruVNSsBbBM7xjwH8iLmxQ=
У такого решения всё ещё имеется недостаток. Поскольку библиотека стала модулем, наш собственный сервис тоже должен импортировать её через свой go.mod. Мы не можем в рамках одного pull request модифицировать и код библиотеки, и код сервиса. Мы потеряли большинство преимуществ хранения библиотеки в репозитории с сервисом. Схожего результата можно было бы добиться простым выносом библиотечного кода в отдельный репозиторий.
Replace
К счастью, и у этой проблемы есть решение. При помощи директивы replace в файле go.mod мы можем подсказать системе модулей, что хотим использовать локальный код нашей библиотеки, а не подтягивать её как версионированную зависимость.
module github.com/LopatkinEvgeniy/go-pkg-example go 1.17 require ( github.com/LopatkinEvgeniy/go-pkg-example/pkg/mylib v0.0.0-00010101000000-000000000000 … ) replace github.com/LopatkinEvgeniy/go-pkg-example/pkg/mylib => ./pkg/mylib
Теперь мы можем продолжать работать с экспортируемыми библиотеками так, как работали бы с локальными пакетами. При этом клиенты могут импортировать эти библиотеки, используя их версионирование и подтягивания только необходимые зависимости.
Краткое решение
-
Переносим экспортируемый код в директорию pkg.
-
Делаем в директории c библиотекой Golang-модуль и создаём git-теги, включающие в себя путь до экспортируемых модулей и их версии.
-
Добавляем директиву replace для экспортируемых библиотек в go.mod нашего сервиса.
Для примера смотрите мой репозиторий, каждый шаг представлен в нём отдельным коммитом.
Заключение
Как видите, существует удобный способ шерить модули, в котором:
-
экспортирующий сервис продолжает работать с библиотечным кодом как со своим собственным,
-
клиенты могут пользоваться версионированием и избежать лишних зависимостей.
Если у вас есть примеры использования экспортируемых модулей, делитесь в комментариях.
Что еще почитать
-
Статья про историю и использование pkg.
-
Обзор Go Workspaces.
ссылка на оригинал статьи https://habr.com/ru/company/ozontech/blog/668254/
Добавить комментарий