Грядущий релиз версии 1.11 языка программирования Go принесет экспериментальную поддержку модулей — новую систему управления зависимостями для Go. (прим.перев.: релиз состоялся)
Недавно я уже писал об этом небольшой пост. С тех пор кое-что слегка поменялось, да и мы стали ближе к релизу, поэтому мне кажется, что настало время для новой статьи — добавим больше практики.
Итак, вот, что мы будем делать: создадим новый пакет и потом сделаем несколько релизов, чтобы посмотреть, как это работает.
Создание модуля
Первым делом создадим наш пакет. Назовём его "testmod". Важная деталь: каталог пакета следует разместить за пределами вашего $GOPATH
, потому что, внутри него по умолчанию отключена поддержка модулей. Модули Go — это первый шаг к полному отказу в будущем от $GOPATH
.
$ mkdir testmod
$ cd testmod
Наш пакет весьма прост:
package testmod
import "fmt"
// Hi returns a friendly greeting
func Hi(name string) string {
return fmt.Sprintf("Hi, %s", name)
}
Пакет готов, но он ещё пока не является модулем. Давайте исправим это.
$ go mod init github.com/robteix/testmod
go: creating new go.mod: module github.com/robteix/testmod
У нас появился новый файл с именем go.mod
в каталоге пакета со следующим содержимым:
module github.com/robteix/testmod
Немного, но именно это и превращает наш пакет в модуль.
Теперь мы можем запушить этот код в репозиторий:
$ git init
$ git add *
$ git commit -am "First commit"
$ git push -u origin master
До сих пор, любой желающий использовать наш пакет применил бы go get
:
$ go get github.com/robteix/testmod
И эта команда принесла бы самый свежий код из ветки master
. Такой вариант все ещё работает, но лучше бы нам так больше не делать, ведь теперь "есть способ лучше". Забирать код прямо из ветки master
, по сути, опасно, поскольку мы никогда не знаем наверняка, что авторы пакета не сделали изменений, которые "сломают" наш код. Для решения именно этой проблемы и были придуманы модули Go.
Небольшое отступление о версионировании модулей
Модули Go — версионируемы, плюс есть некоторая специфичность отдельных версий. Вам придется познакомится с концепциями, лежащими в основе семантического версионирования.
К тому же, Go использует метки репозитория, когда ищет версии, а некоторые версии отличаются от остальных: например, версии 2 и более должны иметь другой путь импорта, чем для версий 0 и 1 (мы дойдем до этого).
По умолчанию, Go загружает самую свежую версию, имеющую метку, доступную в репозитории.
Это важная особенность, поскольку её можно использовать при работе с веткой master
.
Для нас сейчас важно то, что, создавая релиз нашего пакета, нам необходимо поставить метку с версией в репозитории.
Давайте это и сделаем.
Делаем свой первый релиз
Наш пакет готов и мы можем "зарелизить" его на весь мир. Сделаем это с помощью версионных меток. Пусть номер версии будет 1.0.0:
$ git tag v1.0.0
$ git push --tags
Эти команды создают метку в моём Github-репозитории, помечающую текущий коммит как релиз 1.0.0.
Go не настивает на этом, но хорошей идеей будет создать дополнительно новую ветку ("v1"), в которую мы можем отправлять исправления.
$ git checkout -b v1
$ git push -u origin v1
Теперь мы можем работать в ветке master
не беспокоясь, что можем сломать наш релиз.
Использование нашего модуля
Давайте используем созданный модуль. Мы напишем простую программу, которая импортирует наш новый пакет:
package main
import (
"fmt"
"github.com/robteix/testmod"
)
func main() {
fmt.Println(testmod.Hi("roberto"))
}
До сих пор, вы запускали бы go get github.com/robteix/testmod
, чтобы скачать пакет, но с модулями становится интереснее. Для начала нам надо включить поддержку модулей в нашей новой программе.
$ go mod init mod
Как вы наверняка и ожидали, исходя из прочитанного ранее, в каталоге появился новый файл go.mod
с именем модуля внутри:
module mod
Ситуация становится ещё интереснее, когда мы попытаемся собрать нашу программу:
$ go build
go: finding github.com/robteix/testmod v1.0.0
go: downloading github.com/robteix/testmod v1.0.0
Как видно, команда go
автоматически нашла и загрузила пакет, импортируемый нашей программой.
Если мы проверим наш файл go.mod
, мы увидим, что кое-что изменилось:
module mod
require github.com/robteix/testmod v1.0.0
И у нас появился ещё один новый файл с именем go.sum
, который содержит хэши пакетов, чтобы проверять правильность версии и файлов.
github.com/robteix/testmod v1.0.0 h1:9EdH0EArQ/rkpss9Tj8gUnwx3w5p0jkzJrd5tRAhxnA=
github.com/robteix/testmod v1.0.0/go.mod h1:UVhi5McON9ZLc5kl5iN2bTXlL6ylcxE9VInV71RrlO8=
Делаем релиз релиз с исправлением ошибки
Теперь, скажем, мы нашли проблему в нашем пакете: в приветствии отсутствует пунктуация!
Некоторые люди взбесятся, ведь наше дружелюбное приветствие уже не такое и дружелюбное.
Давайте исправим это и выпустим новую версию:
// Hi returns a friendly greeting
func Hi(name string) string {
- return fmt.Sprintf("Hi, %s", name)
+ return fmt.Sprintf("Hi, %s!", name)
}
Мы сделали это изменение прямо в ветке v1
, потому что оно не имеет отношения к тому, что мы будет делать дальше в ветке v2
, но в реальной жизни, возможно, вам следовало бы внести эти изменения в master
и уже потом бэкпортировать их в v1
. В любом случае, исправление должно оказаться в ветке v1
и нам надо отметить это как новый релиз.
$ git commit -m "Emphasize our friendliness" testmod.go
$ git tag v1.0.1
$ git push --tags origin 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 get -u=patch
$ go get github.com/robteix/testmod@v1.0.1
После запуска (допустим, go get -u
), наш go.mod
изменился:
module mod
require github.com/robteix/testmod v1.0.1
Мажорные версии
В соответствии со спецификацией семантического версионирования, мажорная версия отличается от минорных. Мажорные версии могут ломать обратную совместимость. С точки зрения Go модулей, мажорная версия — это совершенно другой пакет.
Может и звучит дико поначалу, но это имеет смысл: две версии библиотеки, которые несовместимы между собой, являются двумя разными библиотеками.
Давайте сделаем мажорное изменение в нашем пакете. Допустим, со временем, нам стало ясно, что наш API слишком прост, слишком ограничен для "юзкейсов" наших пользователей, поэтому нам надо изменить функцию Hi()
, чтобы она принимала язык приветствия в качестве параметра:
package testmod
import (
"errors"
"fmt"
)
// Hi returns a friendly greeting in language lang
func Hi(name, lang string) (string, error) {
switch lang {
case "en":
return fmt.Sprintf("Hi, %s!", name), nil
case "pt":
return fmt.Sprintf("Oi, %s!", name), nil
case "es":
return fmt.Sprintf("¡Hola, %s!", name), nil
case "fr":
return fmt.Sprintf("Bonjour, %s!", name), nil
default:
return "", errors.New("unknown language")
}
}
Существующие программы, использующие наш API, сломаются, потому что они а) не передают язык в качестве параметра и б) не ожидают возврата ошибки. Наш новый API более не совместим с версией 1.x, так что встречайте версию 2.0.0.
Ранее я упоминал, что некоторые версии имеют особенности, и вот сейчас такой случай.
Версии 2 и более должны сменить путь импорта. Теперь это разные библиотеки.
Мы сделаем это добавив новый версионный путь к названию нашего модуля.
module github.com/robteix/testmod/v2
Всё остальное то же самое: пушим, ставим метку, что это v2.0.0 (и опционально содаём ветку v2)
$ git commit testmod.go -m "Change Hi to allow multilang"
$ git checkout -b v2 # optional but recommended
$ echo "module github.com/robteix/testmod/v2" > go.mod
$ git commit go.mod -m "Bump version to v2"
$ git tag v2.0.0
$ git push --tags origin v2 # or master if we don't have a branch
Обновление мажорной версии
Даже при том, что мы зарелизили новую несовместимую версию нашей библиотеки, существующие программы не сломались, потому что они продолжают исполользовать версию 1.0.1.
go get -u
не будет загружать версию 2.0.0.
Но в какой-то момент, я, как пользователь библиотеки, могу захотеть обновиться до версии 2.0.0, потому что, например, я один из тех пользователей, которым нужна поддержка нескольких языков.
Чтобы обновиться, надо соответствующим образом изменить мою программу:
package main
import (
"fmt"
"github.com/robteix/testmod/v2"
)
func main() {
g, err := testmod.Hi("Roberto", "pt")
if err != nil {
panic(err)
}
fmt.Println(g)
}
Теперь, когда я запущу go build
, он "сходит" и загрузит для меня версию 2.0.0. Обратите внимание, хотя путь импорта теперь заканчивается на "v2", Go всё ещё ссылается на модуль по его настоящему имени ("testmod").
Как я уже говорил, мажорная версия — это во всех отношениях другой пакет. Эти два модуля Go никак не связаны. Это значит, что у нас может быть две несовместимые версии в одном бинарнике:
package main
import (
"fmt"
"github.com/robteix/testmod"
testmodML "github.com/robteix/testmod/v2"
)
func main() {
fmt.Println(testmod.Hi("Roberto"))
g, err := testmodML.Hi("Roberto", "pt")
if err != nil {
panic(err)
}
fmt.Println(g)
}
И это избавляет от распространенной проблемы с управлением зависимостями, когда зависимости зависят от разных версий одной и той же библиотеки.
Наводим порядок
Вернёмся к предыдущей версии, которая использует только testmod 2.0.0 — если мы сейчас проверим содержимое go.mod
, мы кое-что заметим:
module mod
require github.com/robteix/testmod v1.0.1
require github.com/robteix/testmod/v2 v2.0.0
По умолчанию, Go не удаляет зависимости из go.mod
, пока вы об этом не попросите. Если у вас есть зависимости, которые больше не нужны, и вы хотите их почистить, можно воспользоваться новой командой tidy
:
$ go mod tidy
Теперь у нас остались только те зависимости, которые мы реально используем.
Вендоринг
Модули Go по умолчанию игнорируют каталог vendor/
. Идея в том, чтобы постепенно избавиться от вендоринга1. Но если мы все ещё хотим добавить "отвендоренные" зависимости в наш контроль версий, мы можем это сделать:
$ go mod vendor
Команда создаст каталог vendor/
в корне нашего проекта, содержащий исходный код всех зависимостей.
Однако, go build
по умолчанию все ещё игнорирует содержимое этого каталога. Если вы хотите собрать зависимости из каталога vendor/
, надо об этом явно попросить.
$ go build -mod vendor
Я предполагаю, что многие разработчики, желающие использовать вендоринг, будут запускать go build
, как обычно, на своих машинах и использовать -mod vendor
на своих CI.
Повторюсь, модули Go уходят от идеи вендоринга к использованию прокси для модулей для тех, кто не хочет напрямую зависеть от вышестоящих служб контроля версий.
Есть способы гарантировать, что go
будет недоступна сеть (например, с помощью GOPROXY=off
), но это уже тема следующей статьи.
Заключение
Статья кому-то может показаться сложноватой, но это из-за того, что я попытался объяснить многое разом. Реальность же такова, что модули Go сегодня в целом просты — мы, как обычно, импортируем пакет в наш код, а остальное за нас делает команда go
. Зависимости при сборке загружаются автоматически.
Модули также избавляют от необходимости в $GOPATH
, которая была камнем преткновения для новых разработчиков Go, у кого были проблемы с пониманием, почему надо что-то положить в какой-то конкретный каталог.
Вендоринг (неофициально) объявлен устаревшим в пользу использования прокси.1
Я могу сделать отдельную статью про прокси для Go модулей.
Примечания:
1 Я думаю, что это слишком громкое выражение и у некоторых может остаться впечатление, что вендоринг убирают прямо сейчас. Это не так. Вендоринг все ещё работает, хотя и слегка по другому, чем раньше. По-видимому, есть желание заменить вендоринг чем-то лучшим, например, прокси (не факт). Пока это просто стремление к лучшему решению. Вендоринг не уйдет, пока не будет найдена хорошая замена (если будет).
Автор: kilgur