Хабрахабр

[Перевод] Введение в систему модулей Go

11 языка программирования Go принесет экспериментальную поддержку модулей — новую систему управления зависимостями для Go. Грядущий релиз версии 1. (прим.перев.: релиз состоялся)

С тех пор кое-что слегка поменялось, да и мы стали ближе к релизу, поэтому мне кажется, что настало время для новой статьи — добавим больше практики. Недавно я уже писал об этом небольшой пост.

Итак, вот, что мы будем делать: создадим новый пакет и потом сделаем несколько релизов, чтобы посмотреть, как это работает.

Создание модуля

Назовём его "testmod". Первым делом создадим наш пакет. Модули Go — это первый шаг к полному отказу в будущем от $GOPATH. Важная деталь: каталог пакета следует разместить за пределами вашего $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. Для решения именно этой проблемы и были придуманы модули Go. Забирать код прямо из ветки master, по сути, опасно, поскольку мы никогда не знаем наверняка, что авторы пакета не сделали изменений, которые "сломают" наш код.

Небольшое отступление о версионировании модулей

Вам придется познакомится с концепциями, лежащими в основе семантического версионирования. Модули Go — версионируемы, плюс есть некоторая специфичность отдельных версий.

К тому же, Go использует метки репозитория, когда ищет версии, а некоторые версии отличаются от остальных: например, версии 2 и более должны иметь другой путь импорта, чем для версий 0 и 1 (мы дойдем до этого).

По умолчанию, Go загружает самую свежую версию, имеющую метку, доступную в репозитории.
Это важная особенность, поскольку её можно использовать при работе с веткой master.

Для нас сейчас важно то, что, создавая релиз нашего пакета, нам необходимо поставить метку с версией в репозитории.

Давайте это и сделаем.

Делаем свой первый релиз

Сделаем это с помощью версионных меток. Наш пакет готов и мы можем "зарелизить" его на весь мир. 0. Пусть номер версии будет 1. 0:

$ git tag v1.0.0
$ git push --tags

0. Эти команды создают метку в моём Github-репозитории, помечающую текущий коммит как релиз 1. 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 и нам надо отметить это как новый релиз. Мы сделали это изменение прямо в ветке v1, потому что оно не имеет отношения к тому, что мы будет делать дальше в ветке v2, но в реальной жизни, возможно, вам следовало бы внести эти изменения в master и уже потом бэкпортировать их в v1.

$ git commit -m "Emphasize our friendliness" testmod.go
$ git tag v1.0.1
$ git push --tags origin v1

Обновление модулей

"И это хорошо", поскольку нам всем хотелось бы предсказуемости в наших сборках. По умолчанию, Go не обновляет модули без спроса. 11". Если бы модули Go обновлялись бы автоматически каждый раз, когда выходит новая версия, мы вернулись бы в "тёмные века до-Go1. Но нет, нам надо сообщить Go, чтобы он обновил для нас модули.

А сделаем мы это с помощью нашего старого друга — go get:

  • команда обновит с 1. запускаем go get -u, чтобы использовать последний минорный или патч- релиз (т.е. 0 до, скажем, 1. 0. 1 или до 1. 0. 0, если такая версия доступна) 1.

  • пакет обновится до 1. запускаем go get -u=patch чтобы использовать последнюю патч-версию (т.е. 1, но не до 1. 0. 0) 1.

  • 0. запускаем go get package@version, чтобы обновиться до конкретной версии (например, github.com/robteix/testmod@v1. 1)

На то есть весомая причина, как мы вскоре увидим. В этом списке нет способа обновиться до последней мажорной версии.

0. Поскольку наша программа использовала версию 1. 0. 0 нашего пакета и мы только что создали версию 1. 0. 1, любая из следующих команд обновит нас до 1. 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)
}

Наш новый API более не совместим с версией 1.x, так что встречайте версию 2. Существующие программы, использующие наш API, сломаются, потому что они а) не передают язык в качестве параметра и б) не ожидают возврата ошибки. 0. 0.

Ранее я упоминал, что некоторые версии имеют особенности, и вот сейчас такой случай.
Версии 2 и более должны сменить путь импорта. Теперь это разные библиотеки.

Мы сделаем это добавив новый версионный путь к названию нашего модуля.

module github.com/robteix/testmod/v2

0. Всё остальное то же самое: пушим, ставим метку, что это v2. 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

Обновление мажорной версии

0. Даже при том, что мы зарелизили новую несовместимую версию нашей библиотеки, существующие программы не сломались, потому что они продолжают исполользовать версию 1. 0. 1.
go get -u не будет загружать версию 2. 0.

0. Но в какой-то момент, я, как пользователь библиотеки, могу захотеть обновиться до версии 2. 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)
}

0. Теперь, когда я запущу go build, он "сходит" и загрузит для меня версию 2. Обратите внимание, хотя путь импорта теперь заканчивается на "v2", Go всё ещё ссылается на модуль по его настоящему имени ("testmod"). 0.

Эти два модуля 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)
}

И это избавляет от распространенной проблемы с управлением зависимостями, когда зависимости зависят от разных версий одной и той же библиотеки.

0. Вернёмся к предыдущей версии, которая использует только testmod 2. 0 — если мы сейчас проверим содержимое go.mod, мы кое-что заметим:

module mod
require github.com/robteix/testmod v1.0.1
require github.com/robteix/testmod/v2 v2.0.0

Если у вас есть зависимости, которые больше не нужны, и вы хотите их почистить, можно воспользоваться новой командой tidy: По умолчанию, Go не удаляет зависимости из go.mod, пока вы об этом не попросите.

$ go mod tidy

Теперь у нас остались только те зависимости, которые мы реально используем.

Вендоринг

Идея в том, чтобы постепенно избавиться от вендоринга1. Модули Go по умолчанию игнорируют каталог vendor/. Но если мы все ещё хотим добавить "отвендоренные" зависимости в наш контроль версий, мы можем это сделать:

$ go mod vendor

Команда создаст каталог vendor/ в корне нашего проекта, содержащий исходный код всех зависимостей.

Если вы хотите собрать зависимости из каталога vendor/, надо об этом явно попросить. Однако, go build по умолчанию все ещё игнорирует содержимое этого каталога.

$ go build -mod vendor

Я предполагаю, что многие разработчики, желающие использовать вендоринг, будут запускать go build, как обычно, на своих машинах и использовать -mod vendor на своих CI.

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

Есть способы гарантировать, что go будет недоступна сеть (например, с помощью GOPROXY=off), но это уже тема следующей статьи.

Заключение

Реальность же такова, что модули Go сегодня в целом просты — мы, как обычно, импортируем пакет в наш код, а остальное за нас делает команда go. Статья кому-то может показаться сложноватой, но это из-за того, что я попытался объяснить многое разом. Зависимости при сборке загружаются автоматически.

Модули также избавляют от необходимости в $GOPATH, которая была камнем преткновения для новых разработчиков Go, у кого были проблемы с пониманием, почему надо что-то положить в какой-то конкретный каталог.

1
Я могу сделать отдельную статью про прокси для Go модулей. Вендоринг (неофициально) объявлен устаревшим в пользу использования прокси.

Примечания:

Это не так. 1 Я думаю, что это слишком громкое выражение и у некоторых может остаться впечатление, что вендоринг убирают прямо сейчас. По-видимому, есть желание заменить вендоринг чем-то лучшим, например, прокси (не факт). Вендоринг все ещё работает, хотя и слегка по другому, чем раньше. Вендоринг не уйдет, пока не будет найдена хорошая замена (если будет). Пока это просто стремление к лучшему решению.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»