Отсутствие в 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
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)


  1. outcoldman
    10.06.2015 04:32
    -2

    Меня честно говоря удивляет то, что в golang такой ограниченный package manager по умолчанию. Не удивляюсь, почему наплодили столько tools github.com/golang/go/wiki/PackageManagementTools


    1. voidnugget
      10.06.2015 06:27
      +2

      Тому есть несколько причин:
      1. Любой уважающий себя рубист/питонист, будучи полным «не очень» с go, будет желать написания своего любимого bundler'a/pip'a etc на go
      2. Было время когда на golang'e было написано много посредственного кода «без фатальных недостатков» как у других, просто потому что людей которые могли написать что-то «более-менее» ещё не было в природе.

      Вот Дейв Чейни — один из немногих, который знает как делать правильно, и в итоге в gb есть не только хорошая сборка и менеджмент зависимостей, но и оптимизации флагов сборки для каждой версии golang'a.


    1. divan0 Автор
      10.06.2015 10:43
      +4

      Почему удивляет? Добрая половина данной статьи объясняет причину этого :)


  1. ivanzoid
    10.06.2015 10:17

    А есть эквивалент команды

    go get ./...
    
    ?

    Или для каждой зависимости нужно делать
    gb vendor fetch foo.com/bar/baz
    
    ?


    1. divan0 Автор
      10.06.2015 10:42

      Пока что нужно делать, но скоро такая фича будет: github.com/constabulary/gb/issues/139


  1. Stronix
    10.06.2015 11:49

    Зачастую, когда Go используется только для одного проекта, рекомендуют подход «GOPATH per project» — фактически, вас просят поменять ваш GOPATH на путь к данному проекту и работать в нём. gb реализует что-то подобное, только не трогая ваш системный GOPATH.

    Но ведь основной GOPATH и не нужно заменять, его можно просто дополнить
    export GOPATH=$HOME/Go:$HOME/project
    Тогда go get будет устанавливать по основному пути, а при импортировании пакет будет искаться по всем путям.


    1. divan0 Автор
      10.06.2015 11:55
      +5

      Все так, но это источник труднодиагностируемых проблем — к примеру, если в project/ у вас более новая версия пакаджа и вы подразумеваете, что работаете с ним, а на самом деле он берется из первого GOPATH. Или, новый разработчик в тиме, забыл/не понял установить два GOPATH и собирает проект, а код берется не завендоренный, а из его GOPATH. Там много всего может быть, поэтому множественный GOPATH и не рекомендуют в общем случае.


      1. Stronix
        10.06.2015 12:02

        Тоже верно…


  1. tzlom
    11.06.2015 00:24
    -1

    go get github.com/constabulary/gb/...

    По непонятной для меня причине программистов на маках тянет к слаке


    1. divan0 Автор
      11.06.2015 02:29
      +1

      Эм… Какая связь между go get, маками и слакой?


      1. Zelgadis
        16.06.2015 03:18
        +1

        Потому, что срач в /usr это традиция слаки. Автор как бы намекает, что правильно было бы каждая зависимость аккуратно упакована в своей пакет и доступна везде. А не 9001 минорная версия одного и того же пакета для каждой программы на Go (по коммиту на каждый пакет).

        Вот gem из руби позволяет иметь несколько версий одного и тожего же пакета в системе, bundler решает проблемы чтобы в приложении была только одна версия пакета (пример npm где каждая зависимость тянет все зависимости локально для себя).

        Maven (а так же остальные) решает проблему тоже весьма интересным способом.

        А что делает Go? «Вот тебе GOPATH ты его меняй на каждый проект и все будет хорошо» В результате пакеты (тут я уже про .dev, .rpm, pkgng) становятся толстыми ибо каждый раз надо все зависимости с собой паковать.Я раньше тоже не понимал зачем в портах фряхи руби гемы лежат, теперь понял.


        1. divan0 Автор
          16.06.2015 10:28
          +1

          Мне кажется, или вы только что попытались сказать «Все вопросы менеджмента зависимостей — легко и просто решаются, но авторы и коммьюнити Go просто не знают того, что знаю я про опыт других языков, и поэтому сделали неправильно»? :)


          1. Zelgadis
            16.06.2015 19:11

            Нет, я ответил на ваш вопрос «Какая связь между go get, маками и слакой?»

            А так же возмутился тем, что все пытаются решить проблему зависимостей, а ребята из go решили не решать ее и сказать «да будет срач»


            1. divan0 Автор
              16.06.2015 20:12

              Ну, во-первых, не ответили. ) В оригинальном комментарии была процитирована строка запуска go get и вот та фраза про связь. Даже оставив в стороне про «срач в /usr» и GOPATH (тут я еще могу ход мысли проследить), но причем тут маки?

              Ваше возмущение мне более-менее понятно, но они да, решают, проблему, и стадия «посмотреть что и как нужно всем» — это тоже часть решения. Это же не арифметическая задача, здесь вопрос компромиссов и не впаривать всем решение «удобное для нас» — достаточно разумно.


              1. Zelgadis
                16.06.2015 20:39

                Притом, что на маках это принято так делать? «chown -R `whoami` /usr» Наверное первое, что програмист который пользуется маком пишем в своем терминале. Вы не знаете менталитет пользователей слаки?


                1. divan0 Автор
                  16.06.2015 20:54

                  Вы не знаете менталитет пользователей слаки?

                  Нет.
                  И я продолжаю не видеть связи между слакой, маком, командой chown и go get. И даже, если я сейчас эту многоходовочку мыслей ухвачу, мне все равно вывод «пользователей мака тянет к слаке» кажется очень неверным :)


                  1. Zelgadis
                    16.06.2015 21:01

                    Это потому, что у вас менталитет пользователя слаки и не замечаете всю грусть ситуации. Вероятно считаете это нормой.


                    1. divan0 Автор
                      16.06.2015 21:39

                      Грусть ситуации в том, что я трачу массу времени, чтобы расшифровать нелогичное утверждение и слушаю непонятные мне ярлыки.