(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 <<END
package main
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
L-- src
L-- myserver
L-- 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
¦ L-- myserver
¦ L-- main.go
L-- vendor
+-- manifest
L-- src
L-- github.com
L-- labstack
L-- 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
L-- 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/
L-- 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.io
Github-репозиторий: github.com/constabulary/gb
Блог пост от автора: dave.cheney.net/2015/06/09/gb-a-project-based-build-tool-for-the-go-programming-language
Design rationale: getgb.io/rationale
Theory of operation: getgb.io/theory
Комментарии (18)
ivanzoid
10.06.2015 10:17А есть эквивалент команды
?go get ./...
Или для каждой зависимости нужно делать
?gb vendor fetch foo.com/bar/baz
divan0 Автор
10.06.2015 10:42Пока что нужно делать, но скоро такая фича будет: github.com/constabulary/gb/issues/139
Stronix
10.06.2015 11:49Зачастую, когда Go используется только для одного проекта, рекомендуют подход «GOPATH per project» — фактически, вас просят поменять ваш GOPATH на путь к данному проекту и работать в нём. gb реализует что-то подобное, только не трогая ваш системный GOPATH.
Но ведь основной GOPATH и не нужно заменять, его можно просто дополнить
Тогда go get будет устанавливать по основному пути, а при импортировании пакет будет искаться по всем путям.export GOPATH=$HOME/Go:$HOME/project
divan0 Автор
10.06.2015 11:55+5Все так, но это источник труднодиагностируемых проблем — к примеру, если в project/ у вас более новая версия пакаджа и вы подразумеваете, что работаете с ним, а на самом деле он берется из первого GOPATH. Или, новый разработчик в тиме, забыл/не понял установить два GOPATH и собирает проект, а код берется не завендоренный, а из его GOPATH. Там много всего может быть, поэтому множественный GOPATH и не рекомендуют в общем случае.
tzlom
11.06.2015 00:24-1go get github.com/constabulary/gb/...
По непонятной для меня причине программистов на маках тянет к слакеdivan0 Автор
11.06.2015 02:29+1Эм… Какая связь между go get, маками и слакой?
Zelgadis
16.06.2015 03:18+1Потому, что срач в /usr это традиция слаки. Автор как бы намекает, что правильно было бы каждая зависимость аккуратно упакована в своей пакет и доступна везде. А не 9001 минорная версия одного и того же пакета для каждой программы на Go (по коммиту на каждый пакет).
Вот gem из руби позволяет иметь несколько версий одного и тожего же пакета в системе, bundler решает проблемы чтобы в приложении была только одна версия пакета (пример npm где каждая зависимость тянет все зависимости локально для себя).
Maven (а так же остальные) решает проблему тоже весьма интересным способом.
А что делает Go? «Вот тебе GOPATH ты его меняй на каждый проект и все будет хорошо» В результате пакеты (тут я уже про .dev, .rpm, pkgng) становятся толстыми ибо каждый раз надо все зависимости с собой паковать.Я раньше тоже не понимал зачем в портах фряхи руби гемы лежат, теперь понял.divan0 Автор
16.06.2015 10:28+1Мне кажется, или вы только что попытались сказать «Все вопросы менеджмента зависимостей — легко и просто решаются, но авторы и коммьюнити Go просто не знают того, что знаю я про опыт других языков, и поэтому сделали неправильно»? :)
Zelgadis
16.06.2015 19:11Нет, я ответил на ваш вопрос «Какая связь между go get, маками и слакой?»
А так же возмутился тем, что все пытаются решить проблему зависимостей, а ребята из go решили не решать ее и сказать «да будет срач»divan0 Автор
16.06.2015 20:12Ну, во-первых, не ответили. ) В оригинальном комментарии была процитирована строка запуска go get и вот та фраза про связь. Даже оставив в стороне про «срач в /usr» и GOPATH (тут я еще могу ход мысли проследить), но причем тут маки?
Ваше возмущение мне более-менее понятно, но они да, решают, проблему, и стадия «посмотреть что и как нужно всем» — это тоже часть решения. Это же не арифметическая задача, здесь вопрос компромиссов и не впаривать всем решение «удобное для нас» — достаточно разумно.Zelgadis
16.06.2015 20:39Притом, что на маках это принято так делать? «chown -R `whoami` /usr» Наверное первое, что програмист который пользуется маком пишем в своем терминале. Вы не знаете менталитет пользователей слаки?
divan0 Автор
16.06.2015 20:54Вы не знаете менталитет пользователей слаки?
Нет.
И я продолжаю не видеть связи между слакой, маком, командой chown и go get. И даже, если я сейчас эту многоходовочку мыслей ухвачу, мне все равно вывод «пользователей мака тянет к слаке» кажется очень неверным :)
outcoldman
Меня честно говоря удивляет то, что в golang такой ограниченный package manager по умолчанию. Не удивляюсь, почему наплодили столько tools github.com/golang/go/wiki/PackageManagementTools
voidnugget
Тому есть несколько причин:
1. Любой уважающий себя рубист/питонист, будучи полным «не очень» с go, будет желать написания своего любимого bundler'a/pip'a etc на go
2. Было время когда на golang'e было написано много посредственного кода «без фатальных недостатков» как у других, просто потому что людей которые могли написать что-то «более-менее» ещё не было в природе.
Вот Дейв Чейни — один из немногих, который знает как делать правильно, и в итоге в gb есть не только хорошая сборка и менеджмент зависимостей, но и оптимизации флагов сборки для каждой версии golang'a.
divan0 Автор
Почему удивляет? Добрая половина данной статьи объясняет причину этого :)