gb — менеджмент зависимостей для Go
Отсутствие в Go нативного менеджера зависимостей и версий является одним из самых частых пунктов в критике языка. В этой статье мы рассмотрим проблему детальнее и познакомимся с новым проектом, с лаконичным именем gb, который набирает популярность в Go-коммьюнити и обещает вскоре стать де-факто стандартом для управления зависимостями и версиями в Go.(Credit orig.photo: Nathan Youngman)
Для начала давайте разберемся, из-за чего весь шум и почему в Go изначально не было продвинутого менеджера зависимостей.
Что такое менеджмент зависимостей? Что же подразумевается под «продвинутым» менеджером зависимостей и какие задачи он должен решать. Как-никак, но в Go простой менеджмент зависимостей присутствует.В Go есть стандартная команда «go get», которой достаточно указать название проекта на, скажем, Github-е, чтобы код проекта был установлен, собран и готов к использованию. «go get» всегда скачивает последнюю версию, из master-ветки, и общий консенсус для open-source библиотек заключается в том, чтобы не ломать обратную совместимость никакой ценой. Об этом написано в FAQ, и в подавляющем большинстве случаев сторонних библиотек, это правило соблюдается.
Вы, в зависимости от вашего предыдущего опыта, можете подобную идею отвергнуть на корню, или предвидеть, что очень скоро эта схема окажется нежизнеспособной, но пока что, через 3 года существования Go 1, эта схема работает и даже используется в продакшене в больших компаниях, хоть изначально и больше расчитана на open-source экосистему. Скажу так — это вопрос соотношения сложности и рисков, которые готовы брать на себя разработчики.
Но перейдя от open-source мира в мир коммерческой разработки, сразу же встает надобность гарантировать, что код определенной ревизии будет всегда компилироваться и выдавать одинаковый результат, и не зависеть от внешних факторов. Под внешними факторами могут подразумеваться, как новые, ломающие совместимость, изменения в third-party библиотеках, так и отключение интернета или падение Github-а.
Задача эта решается двумя способами, которые, зачастую идут вместе — версионированием зависимостей (versioning) и вендорингом (vendoring) — включением third-party кода в вашу кодовую базу. Первое позволяет гарантировать, что новые коммиты в стороннюю библиотеку не поломают ваш билд, а второе — что даже при падении интернета, весь код, необходимый для сборки, будет доступен для сборки локально.
Собственно, это и есть ответ на вопрос, что такое продвинутый менеджмент зависимостей — это инструмент, включающий в себя всё необходимое для версионирования и вендоринга зависимостей.
Почему этого нет в Go? Краткий ответ — потому что это чертовски сложно сделать так, чтобы всех устраивало. Не то что сложно — это невозможно, если вы хотите сделать максимально удобный инструмент, подходящий для всех случаев, случающихся в реальной практике.Авторы Go изначально признали сложность задачи, и честно отказались навязывать всем какое-то решение, предложив варианты. Этот вопрос хорошо изложен в официальном FAQ и ответ звучит следующим образом:
Версионирование это источник значительной сложности, особенно в больших кодовых базах, и мы не знаем такого решения, которое бы, в масштабе всего разнообразия возможных ситуаций, работало бы достаточно хорошо для всех.
Достаточно честно, согласитесь.Далее в FAQ даются рекомендации, как и что делать, и как эту задачу Google решает в своем случае (опять же, делая акцент, что «то что подходит для нас, может не подходить для вас»). И если существует «идеальное решение», то у коммьюнити есть все карты на руках, чтобы его создать и предложить в качестве стандартного для Go.
Как показала практика, универсального решения для всех тут нет. Зато зоопарк решений для разных случаев и постоянное обсуждение вопроса привело к достаточно неплохому консенсусу на то, что же всё таки положит конец спорам про продвинутый менеджмент зависимостей в Go.
Краткая история попыток решить проблему. Весь зоопарк решений сводится к утилитам, которые решают вопрос либо версионирования, либо вендоринга, либо того и другого. Спектр решений широк — от простых Makefile-c с заменой GOPATH и git-самодулей до перезаписи путей в импортах и полноценных all-in-one утилит, самой популярной из которых является Godep.Но все они по своей сути являются врапперами над стандартными командами go get и go build. И именно это было источником главных несостыковок. Workflow при работе с go get/build великолепно работает, если вы пишете open-source проект — все просто и удобно. Но если вы пишете отдельный рабочий проект, который никак вообще даже не пересекается с вашей open-source деятельностью, то все решения для версионирования/вендоринга становятся занозой в пятке и лишают определенных удобств и добавляют сложности. Ещё запутанней всё становится, если вы пытаетесь смешивать оба сценария.
Осознание этих двух главных различных сценариев разработки (open-source vs private closed project) привело к пониманию того, что различные сценарии требуют различных решений. И не так давно, Dave Cheney (один из Go-контрибьюторов и вообще, знатный, Go-популяризатор) предложил четко разделить эти два сценария, создав для второго отдельный инструмент, который будет похож на стандартный go get/build, но изначально созданный для работы с project-ориентированным деревом исходников, с четким разделением на «свой код» и «зависимости».
gb — project-based build tool Основные тезисы, лежащие в основе gb:
отдельная утилита, заменяющая стандартную go get/build/test всё определяет структура директории никаких специальных файлов описаний никаких изменений кода (в т.ч. перезаписи импортов) четкое разделение кода на «проект» и «зависимости» Если вы испугались на фразе «заменяет стандартную go get/build/test», то это нормальная реакция :) На самом деле проект для gb можно абсолютно спокойно использовать и со стандартными go build/test, но gb build позволит вам не заморачиваться на пути к завендоренным пакетам.Теперь по-порядку.
Отдельная утилита, заменяющая стандартную go get/build/testЭто именно так, и вам придётся поставить в свою рабочую систему ещё одну команду. Делается это просто:
go get github.com/constabulary/gb/… Всё определяет структура директорииПод «всё» подразумевается факт того, что gb сможет работать с этим проектом. Правило тут простое — в директории должна быть папка src/. И этого достаточно, чтобы начать работать с gb.Зачастую, когда Go используется только для одного проекта, рекомендуют подход «GOPATH per project» — фактически, вас просят поменять ваш GOPATH на путь к данному проекту и работать в нём. gb реализует что-то подобное, только не трогая ваш системный GOPATH. Чуть подробнее ниже.
Никаких специальных файлов описанийСовременные проекты и так состоят из десятка .dot-файлов с различными манифестами и описаниями. Ещё один такой файл, необходимый для сборки проекта — это было бы чересчур и не в Go-стиле.
Никаких изменений кодаКод проекта всегда остается таким, каким он и написан. Никаких перезаписей импортов в стиле 'import «github.com/user/pkg» → import «vendor/github.com/user/pkg»' нет и не будет.
Четкое разделение кода на «проект» и «зависимости«Возвращаясь к пункту про структуру директории, gb трактует всё, что находится в src/, как код вашего проекта. Все зависимые пакаджи устанавливаются в директорию vendor/ и именно оттуда код берется при сборке с помощью gb.
Пример использования
Самый лучший способ понять инструмент, это использовать его.Для начала, создадим новый проект, ~/demoproject. Директория значения не имеет, хоть в /tmp. При работе с gb можете забыть про ваш стандартный GOPATH вообще.
mkdir ~/demoproject && cd!$
mkdir -p src/myserver
cat > src/myserver/main.go < import (
«github.com/labstack/echo»
«net/http»
) func hello (c *echo.Context) error {
return c.String (http.StatusOK, «Hello, World!\n»)
} func main () {
e:= echo.New ()
e.Get (»/», hello)
e.Run (»:1323»)
}
END
Дерево проекта у нас пока выглядит вот так:
$ tree $(pwd)
/Users/user/demoproject
└── src
└── myserver
└── main.go
Запускаем билд с помощью команды:
$ gb build
gb build должен выдать ошибку о том, что зависимость (github.com/labstack/echo) не найдена:
FATAL command «build» failed: failed to resolve import path «myserver»: cannot find package «github.com/labstack/echo» in any of:
/usr/local/go/src/github.com/labstack/echo (from $GOROOT)
/Users/user/demoproject/src/github.com/labstack/echo (from $GOPATH)
/Users/user/demoproject/vendor/src/github.com/labstack/echo
Если внимательно посмотреть, то видно, что gb ищет зависимый пакадж сначала в GOROOT (так как stdlib-пакаджи лежат там), затем в GOPATH, который определен для этого проекта (demoproject/src), и, в последнюю очередь, в demoproject/vendor. Поскольку пакета пока нигде нет, получаем ошибку. Можно стянуть пакет руками в vendor (и не забыть удалить папку .git), но у gb для этого есть функционал: gb vendor.
$ gb vendor fetch github.com/labstack/echo
Cloning into '/var/folders/qp/6bvmky410dn8p1yhn3b19yxr0000gn/T/gb-vendor-097548747'…
remote: Counting objects: 1531, done.
remote: Compressing objects: 100% (45/45), done.
remote: Total 1531 (delta 12), reused 0 (delta 0), pack-reused 1482
Receiving objects: 100% (1531/1531), 317.40 KiB | 191.00 KiB/s, done.
Resolving deltas: 100% (911/911), done.
Checking connectivity… done.
Проверяем структуру директории проекта:
$ tree -L 6 $(pwd)
/Users/user/demoproject
├── src
│ └── myserver
│ └── main.go
└── vendor
├── manifest
└── src
└── github.com
└── labstack
└── echo
├── LICENSE
├── README.md
├── context.go
├── context_test.go
├── echo.go
├── echo_test.go
├── examples
├── group.go
├── group_test.go
├── middleware
├── response.go
├── response_test.go
├── router.go
├── router_test.go
└── website 10 directories, 14 files
В manifest-файле записаны вся информация о версиях зависимостей:$ cat vendor/manifest
{
«version»: 0,
«dependencies»: [
{
«importpath»: «github.com/labstack/echo»,
«repository»: «https://github.com/labstack/echo»,
«revision»:»1ac5425ec48d1301d35a5b9a520326d8fca7e036»,
«branch»: «master»
}
]
}
Повторяем gb vendor fetch для остальных зависимостей и пробуем теперь собрать код:
$ gb build
github.com/bradfitz/http2/hpack
github.com/labstack/gommon/color
github.com/mattn/go-colorable
golang.org/x/net/websocket
github.com/bradfitz/http2
github.com/labstack/echo
myserver
$ tree bin/
bin/
└── myserver 0 directories, 1 file
Бинарник кладется, как и в привычном сценарии работы в одном GOPATH в директорию bin/. Теперь всю директорию можно смело вносить в систему контроля версий, и тут уже вам, как хозяину проекта решать — хотите вы оставить только версионирование, или вендорить все зависимости тоже, или всё завендорить, а версии менеджить вручную. В ваших руках свобода выбора. только версионирование: добавляете vendor/src в .gitignore
только вендоринг: добавляете vendor/manifest в .gitignore
и версионирование и вендоринг: оставляете .gitignore без изменений
В случае с версионированием-only любой разработчик из вашей команды после того, как склонировал репозиторий на свою машину, должен запустить:
$ gb vendor update -all
чтобы получить 1-в-1 дерево зависимостей для сборки проекта.В целом этого описания должно быть достаточно, чтобы понять принципы работы и подход gb к решению вопроса версионирования и вендоринга. Послесловие
Проект gb пока находится в ранней стадии развития и принятия сообществом, но судя по реакции последнего и бурной поддержке — это очень удачное решение.У проекта пока достаточно много открытых issues в трекере, но они быстро закрываются и проект активно развивается.По ещё не большому личному опыту использования — есть пока сложности с кросс-платформенной сборкой. В остальном же пока полёт нормальный. Ссылки
Официальный сайт: getgb.ioGithub-репозиторий: github.com/constabulary/gbБлог пост от автора: dave.cheney.net/2015/06/09/gb-a-project-based-build-tool-for-the-go-programming-languageDesign rationale: getgb.io/rationaleTheory of operation: getgb.io/theory