Экспортируем модули из Go-сервиса: сотворение директории pkg

от автора

Чтобы поделиться кодом, нужно создать библиотеку и разместить её в самостоятельном репозитории. Но иногда возникает необходимость хранить библиотеку вместе с сервисом, который её использует. Среди Go-разработчиков существует мнение, что экспортируемые библиотеки стоит хранить в директории pkg

История этой директории берёт начало со времён ранних релизов Go, когда модули стандартной библиотеки находились в $GOROOT/src/pkg. Впоследствии директория pkg была удалена, но многие проекты, такие как Kubernetes, повторили у себя данную файловую структуру. С тех пор pkg закрепилась в файловой структуре Go-проектов.

Когда же лучше хранить библиотечный код в одном репозитории с сервисом? 

  1. При разработке в open source, если не хочется плодить множество отдельных репозиториев.

  2. В процессе дробления монолита на микросервисы. Монолит экспортирует часть своего кода, но при этом остаётся его владельцем. Это позволяет создавать в монолите pull request, который модифицирует одновременно и основной код монолита, и код библиотеки.

  3. При шеринге своим 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

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

Краткое решение

  1. Переносим экспортируемый код в директорию pkg.

  2. Делаем в директории c библиотекой Golang-модуль и создаём git-теги, включающие в себя путь до экспортируемых модулей и их версии.

  3. Добавляем директиву replace для экспортируемых библиотек в go.mod нашего сервиса.

Для примера смотрите мой репозиторий, каждый шаг представлен в нём отдельным коммитом.

Заключение

Как видите, существует удобный способ шерить модули, в котором:

  • экспортирующий сервис продолжает работать с библиотечным кодом как со своим собственным, 

  • клиенты могут пользоваться версионированием и избежать лишних зависимостей. 

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

Что еще почитать


ссылка на оригинал статьи https://habr.com/ru/company/ozontech/blog/668254/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *