Pull to refresh

gb — менеджмент зависимостей для Go

Reading time 8 min
Views 20K
Отсутствие в 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 <<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
└── 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.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
Tags:
Hubs:
+37
Comments 18
Comments Comments 18

Articles