Грядущий релиз версии 1.11 языка программирования Go принесет экспериментальную поддержку модулей — новую систему управления зависимостями для Go. (прим.перев.: релиз состоялся) Недавно я уже писал об этом небольшой пост. С тех пор кое-что слегка поменялось, да и мы стали ближе к релизу, поэтому мне кажется, что настало время для новой статьи — добавим больше практики. Итак, вот, что мы будем делать: создадим новый пакет и потом сделаем несколько релизов, чтобы посмотреть, как это работает. Первым делом создадим наш пакет. Назовём его "testmod". Важная деталь: каталог пакета следует разместить за пределами вашего $GOPATH, потому что, внутри него по умолчанию отключена поддержка модулей. Модули Go — это первый шаг к полному отказу в будущем от $GOPATH. Наш пакет весьма прост: Пакет готов, но он ещё пока не является модулем. Давайте исправим это. У нас появился новый файл с именем go.mod в каталоге пакета со следующим содержимым: Немного, но именно это и превращает наш пакет в модуль. Теперь мы можем запушить этот код в репозиторий: До сих пор, любой желающий использовать наш пакет применил бы go get: И эта команда принесла бы самый свежий код из ветки master. Такой вариант все ещё работает, но лучше бы нам так больше не делать, ведь теперь "есть способ лучше". Забирать код прямо из ветки master, по сути, опасно, поскольку мы никогда не знаем наверняка, что авторы пакета не сделали изменений, которые "сломают" наш код. Для решения именно этой проблемы и были придуманы модули Go. Модули Go — версионируемы, плюс есть некоторая специфичность отдельных версий. Вам придется познакомится с концепциями, лежащими в основе семантического версионирования. К тому же, Go использует метки репозитория, когда ищет версии, а некоторые версии отличаются от остальных: например, версии 2 и более должны иметь другой путь импорта, чем для версий 0 и 1 (мы дойдем до этого). По умолчанию, Go загружает самую свежую версию, имеющую метку, доступную в репозитории.
Это важная особенность, поскольку её можно использовать при работе с веткой master. Для нас сейчас важно то, что, создавая релиз нашего пакета, нам необходимо поставить метку с версией в репозитории. Давайте это и сделаем. Наш пакет готов и мы можем "зарелизить" его на весь мир. Сделаем это с помощью версионных меток. Пусть номер версии будет 1.0.0: Эти команды создают метку в моём Github-репозитории, помечающую текущий коммит как релиз 1.0.0. Go не настивает на этом, но хорошей идеей будет создать дополнительно новую ветку ("v1"), в которую мы можем отправлять исправления. Теперь мы можем работать в ветке master не беспокоясь, что можем сломать наш релиз. Давайте используем созданный модуль. Мы напишем простую программу, которая импортирует наш новый пакет: До сих пор, вы запускали бы go get github.com/robteix/testmod, чтобы скачать пакет, но с модулями становится интереснее. Для начала нам надо включить поддержку модулей в нашей новой программе. Как вы наверняка и ожидали, исходя из прочитанного ранее, в каталоге появился новый файл go.mod с именем модуля внутри: Ситуация становится ещё интереснее, когда мы попытаемся собрать нашу программу: Как видно, команда go автоматически нашла и загрузила пакет, импортируемый нашей программой.
Если мы проверим наш файл go.mod, мы увидим, что кое-что изменилось: И у нас появился ещё один новый файл с именем go.sum, который содержит хэши пакетов, чтобы проверять правильность версии и файлов. Теперь, скажем, мы нашли проблему в нашем пакете: в приветствии отсутствует пунктуация!
Некоторые люди взбесятся, ведь наше дружелюбное приветствие уже не такое и дружелюбное.
Давайте исправим это и выпустим новую версию: Мы сделали это изменение прямо в ветке v1, потому что оно не имеет отношения к тому, что мы будет делать дальше в ветке v2, но в реальной жизни, возможно, вам следовало бы внести эти изменения в master и уже потом бэкпортировать их в v1. В любом случае, исправление должно оказаться в ветке v1 и нам надо отметить это как новый релиз. По умолчанию, Go не обновляет модули без спроса. "И это хорошо", поскольку нам всем хотелось бы предсказуемости в наших сборках. Если бы модули Go обновлялись бы автоматически каждый раз, когда выходит новая версия, мы вернулись бы в "тёмные века до-Go1.11". Но нет, нам надо сообщить Go, чтобы он обновил для нас модули. А сделаем мы это с помощью нашего старого друга — go get: запускаем go get -u, чтобы использовать последний минорный или патч- релиз (т.е. команда обновит с 1.0.0 до, скажем, 1.0.1 или до 1.1.0, если такая версия доступна) запускаем go get -u=patch чтобы использовать последнюю патч-версию (т.е. пакет обновится до 1.0.1, но не до 1.1.0) запускаем go get package@version, чтобы обновиться до конкретной версии (например, github.com/robteix/testmod@v1.0.1) В этом списке нет способа обновиться до последней мажорной версии. На то есть весомая причина, как мы вскоре увидим. Поскольку наша программа использовала версию 1.0.0 нашего пакета и мы только что создали версию 1.0.1, любая из следующих команд обновит нас до 1.0.1: После запуска (допустим, go get -u), наш go.mod изменился: В соответствии со спецификацией семантического версионирования, мажорная версия отличается от минорных. Мажорные версии могут ломать обратную совместимость. С точки зрения Go модулей, мажорная версия — это совершенно другой пакет. Может и звучит дико поначалу, но это имеет смысл: две версии библиотеки, которые несовместимы между собой, являются двумя разными библиотеками. Давайте сделаем мажорное изменение в нашем пакете. Допустим, со временем, нам стало ясно, что наш API слишком прост, слишком ограничен для "юзкейсов" наших пользователей, поэтому нам надо изменить функцию Hi(), чтобы она принимала язык приветствия в качестве параметра: Существующие программы, использующие наш API, сломаются, потому что они а) не передают язык в качестве параметра и б) не ожидают возврата ошибки. Наш новый API более не совместим с версией 1.x, так что встречайте версию 2.0.0. Ранее я упоминал, что некоторые версии имеют особенности, и вот сейчас такой случай.
Версии 2 и более должны сменить путь импорта. Теперь это разные библиотеки. Мы сделаем это добавив новый версионный путь к названию нашего модуля. Всё остальное то же самое: пушим, ставим метку, что это v2.0.0 (и опционально содаём ветку v2) Даже при том, что мы зарелизили новую несовместимую версию нашей библиотеки, существующие программы не сломались, потому что они продолжают исполользовать версию 1.0.1.
go get -u не будет загружать версию 2.0.0. Но в какой-то момент, я, как пользователь библиотеки, могу захотеть обновиться до версии 2.0.0, потому что, например, я один из тех пользователей, которым нужна поддержка нескольких языков. Чтобы обновиться, надо соответствующим образом изменить мою программу: Теперь, когда я запущу go build, он "сходит" и загрузит для меня версию 2.0.0. Обратите внимание, хотя путь импорта теперь заканчивается на "v2", Go всё ещё ссылается на модуль по его настоящему имени ("testmod"). Как я уже говорил, мажорная версия — это во всех отношениях другой пакет. Эти два модуля Go никак не связаны. Это значит, что у нас может быть две несовместимые версии в одном бинарнике: И это избавляет от распространенной проблемы с управлением зависимостями, когда зависимости зависят от разных версий одной и той же библиотеки. Вернёмся к предыдущей версии, которая использует только testmod 2.0.0 — если мы сейчас проверим содержимое go.mod, мы кое-что заметим: По умолчанию, Go не удаляет зависимости из go.mod, пока вы об этом не попросите. Если у вас есть зависимости, которые больше не нужны, и вы хотите их почистить, можно воспользоваться новой командой tidy: Теперь у нас остались только те зависимости, которые мы реально используем. Модули Go по умолчанию игнорируют каталог vendor/. Идея в том, чтобы постепенно избавиться от вендоринга1. Но если мы все ещё хотим добавить "отвендоренные" зависимости в наш контроль версий, мы можем это сделать: Команда создаст каталог vendor/ в корне нашего проекта, содержащий исходный код всех зависимостей. Однако, go build по умолчанию все ещё игнорирует содержимое этого каталога. Если вы хотите собрать зависимости из каталога vendor/, надо об этом явно попросить. Я предполагаю, что многие разработчики, желающие использовать вендоринг, будут запускать go build, как обычно, на своих машинах и использовать -mod vendor на своих CI. Повторюсь, модули Go уходят от идеи вендоринга к использованию прокси для модулей для тех, кто не хочет напрямую зависеть от вышестоящих служб контроля версий. Есть способы гарантировать, что go будет недоступна сеть (например, с помощью GOPROXY=off), но это уже тема следующей статьи. Статья кому-то может показаться сложноватой, но это из-за того, что я попытался объяснить многое разом. Реальность же такова, что модули Go сегодня в целом просты — мы, как обычно, импортируем пакет в наш код, а остальное за нас делает команда go. Зависимости при сборке загружаются автоматически. Модули также избавляют от необходимости в $GOPATH, которая была камнем преткновения для новых разработчиков Go, у кого были проблемы с пониманием, почему надо что-то положить в какой-то конкретный каталог. Вендоринг (неофициально) объявлен устаревшим в пользу использования прокси.1
Я могу сделать отдельную статью про прокси для Go модулей. Примечания: 1 Я думаю, что это слишком громкое выражение и у некоторых может остаться впечатление, что вендоринг убирают прямо сейчас. Это не так. Вендоринг все ещё работает, хотя и слегка по другому, чем раньше. По-видимому, есть желание заменить вендоринг чем-то лучшим, например, прокси (не факт). Пока это просто стремление к лучшему решению. Вендоринг не уйдет, пока не будет найдена хорошая замена (если будет). АСУшник