Некоторые тонкости управления зависимостями в Go

20 ноября 2019

За последние десять лет подход к управлению зависимостями в Go несколько раз переосмыслялся. Все начиналось с «просто используйте go get и никогда не ломайте обратную совместимость». Как ни странно, это не работало. Потом было «Вы все не так поняли — мы не говорили, что менеджер зависимостей не нужен, мы просто не знали, как его сделать! Попробуйте dep ensure». Dep работал уже почти хорошо. Иногда он сыпал непонятными ошибками, но обычно эти ошибки проходили с удалением файла Gopkg.lock и каталога vendor. Сейчас же на смену dep, носившему статус «официального эксперимента», пришел go mod. Это уже совсем настоящий, не экспериментальный, менеджер зависимостей. Вот о паре нюансов, связанных с использованием go mod, мне и хотелось бы рассказать.

Примечание: Для повторения описанных далее шагов вам понадобится Go версии 1.11 или старше. Крайне желательно использовать последнюю на данный момент версию 1.13, так как кое-какие детали меняются между релизами. Также вас может заинтересовать пост Некоторые подводные грабли в языке Go, если вдруг вы его пропустили.

О терминологии

Первый нюанс заключается в терминологии. Это довольно важный момент, и, к сожалению, на нем акцентируется мало внимания. Но если вы уверены, что знаете все тонкости касаемо терминологии, смело переходите к следующему разделу.

Итак, пакетом в Go называется несколько логически связанных между собой файлов с исходным кодом. Когда вы пишите ключевое слово package, вы тем самым говорите, к какому пакету относится файл. Пакеты в основном нужны для того, чтобы в коде возникало поменьше конфликтов имен, а также для разделения процедур на приватные и публичные.

Связанные между собой пакеты образуют модуль. Как правило, один Git-репозиторий соответствует одному модулю, хотя допускается хранить и несколько модулей в одном репозитории. Модуль представляет собой полноценное приложение или библиотеку. Модули имеют версии и зависят от других модулей.

Менеджер пакетов (package manager) — это прижившееся название такой штуки, которая решает, какие зависимости и каких версий нужно поставить для сборки приложения или библиотеки. Вопреки названию, в Go менеджер пакетов работает вовсе не с пакетами, а с модулями. Кроме того, как pip, так и apt называют менеджерами пакетов, хотя они решают довольно разные задачи. Первый качает библиотеки в проектах на языке Python, а второй ставит в Linux пакеты, предназначенные для конечного пользователя. Чтобы всей этой путаницы не возникало, в мире Go принято использовать термин менеджер зависимостей (dependency manager).

И наконец, прокси модулей (module proxy), или просто прокси. Модули можно устанавливать, либо скачивая их напрямую, либо через прокси. Если вам доводилось работать с Artifactory или Nexus, то идея аналогичная. Когда вы скачиваете модули через прокси, прокси их кэширует. Даже если библиотека будет удалена автором, она останется в кэше прокси, и вы сможете собрать проект. Аналогично, прокси поможет собрать проект в случае, если автор одной из зависимостей подвинет тэг.

О странных людях и нерабочих прокси

Второй нюанс заключается в документации. Документация на модули в Go неверна. Точнее говоря, технически она как бы верна. Но на практике, если вы будете строить свое приложение на go mod и следовать документации буквально, вас не ждет ничего кроме боли и разочарования. Почему?

Сценарий первый. Вы не используете в своем проекте прокси. Рано или поздно какой-то странный человек на GitHub решает, к примеру, удалить тэг у своей библиотеки. Назовем ее библиотекой А. Характерно, что вы даже не используете библиотеку А в своем проекте. Однако вы используете библиотеку Б, которая зависит от А, притом именно от удаленного тэга. Результат? Проект взрывается на CI. Вы тратите время, чтобы понять, что произошло. Потом посылаете патч автору библиотеки Б, и вынуждены ждать, пока патч не будет принят. При этом автор библиотеки Б вообще ни в чем не виноват, но ему теперь нужно потратить свое время на решение проблемы.

К сожалению, вот эти истории с удаленными репозиториями, удаленными или подвинутыми тэгами, а то и вовсе зависимостями, живущими на каких-то непонятных доменах у кого-то дома на малинке, случаются чаще, чем хотелось бы. В общем, подход совсем не работает. Проверено.

Сценарий второй. Вы решаете использовать в проекте прокси. Казалось бы, проблема решена. Однако, придя на работу на следующий день, вы видите, что найтли билды опять взорвались. Вы смотрите в логи и видите там ответ от proxy.golang.org:

go: golang.org/x/tools@v0.0.0-20190130014116-16909d206f00: unexpected ⏎
status (https://proxy.golang.org/golang.org/x/tools/@v/v0.0.0-20190130⏎
014116-16909d206f00.info): 410 Gone

«Прокси обеспечивают стабильность сборки» говорили они. Может быть, этот прокси какой-то неправильный? Давайте попробуем goproxy.io. Ух ты, работает! Тогда используем его. Приходим на следующий день — найтли снова взорвались. В логах видим «TLS handshake timeout».

А что, если поднять свой прокси? Поднять-то можно. Но что заставляет вас думать, что с ним сборка магическим образом станет стабильнее? Даже если предположить, что он будет идеально кэшировать все модули, сетевые проблемы никто не отменял. А на момент написания этих строк в go mod даже не было поддержки повторной посылки запросов. Кроме того, возникают вопросы со стороны коллег из отдела безопасности. А хотим ли мы, чтобы прокси торчал наружу? Вообще-то, не хотим. Как минимум, сетевой трафик не бесплатный. Значит, нужна какая-то аутентификация, или доступ должен быть только через VPN. Звучит уже достаточно неудобно, чтобы этого не хотеть. Кроме того, мы же пишем open source. Как прикажете предоставлять доступ контрибьюторам?

В общем и целом, подход с прокси тоже не работает. Проверено.

Как нужно делать на самом деле

Вендоринг (vendoring) — это когда вы складываете все зависимости приложения в каталог vendor и коммитите их прямо в свой репозиторий. Больше никаких хождений по сети. Зависимости больше не могут никуда пропасть. Жизнь стала простой и понятной. Конечно, репозиторий при этом немного разрастется. Но поскольку мы не храним историю всех изменений для зависимостей, это не так страшно. С учетом вышесказанного, давайте перепишем документацию на Go-модули.

Инициируем модуль как обычно:

go mod init github.com/afiskon/go-rest-service-example
git add go.mod

Модуль может находиться в любом каталоге. Наконец-то можно забыть про $GOPATH. Если, тем не менее, по каким-то причинам вы продолжаете держать проекты в $GOPATH, для повторения описанных далее шагов вам понадобится переменная окружения GO111MODULE=on.

Код нашего исполняемого файла будет лежать здесь:

mkdir -p cmd/rest-service-example
touch cmd/rest-service-example/main.go

Сам код:

package main

import (
    "github.com/sirupsen/logrus"
)

func main() {
    logrus.Infof("Hello!")
}

Тянем зависимости:

go mod vendor
git add vendor
git add go.sum

Для сборки проекта воспользуемся небольшим скриптом:

#!/bin/sh

set -e
export GOFLAGS="-mod=vendor"

go build -o bin/rest-service-example cmd/rest-service-example/main.go

Собираем приложение:

chmod u+x build.sh
./build.sh

Наконец, пушим все, включая каталог vendor, в репозиторий:

git commit -am 'Initial commit'
git push origin HEAD

Для сборки проекта с нуля достаточно склонировать репозиторий и запустить build.sh. Этот способ работает, и работает очень хорошо. Проверено.

Заключение

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

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.