The Go Blog

Переход на Go Modules

Джин Бархюйзен
21 августа 2019

Введение

Эта статья является второй в серии.

Примечание: Для документации см. Управление зависимостями и Разработка и публикация модулей.

Проекты на Go используют множество различных стратегий управления зависимостями. Инструменты vendoring, такие как dep и glide, популярны, но они сильно различаются по поведению и не всегда хорошо работают вместе. Некоторые проекты хранят весь свой GOPATH в одном Git-репозитории. Другие просто полагаются на go get и ожидают, что зависимости будут установлены в GOPATH в достаточно свежих версиях.

Система модулей Go, представленная в Go 1.11, предоставляет официальное решение по управлению зависимостями, встроенное в команду go. В этой статье описываются инструменты и техники для преобразования проекта в модули.

Пожалуйста, обратите внимание: если ваш проект уже помечен тегом v2.0.0 или выше, вам потребуется обновить путь модуля при добавлении файла go.mod. Мы объясним, как это сделать без нарушения работы пользователей в будущей статье, посвящённой версиям v2 и далее.

Переход на Go modules в вашем проекте

Проект может находиться в одном из трёх состояний при переходе к Go modules:

  • Новый проект на Go.
  • Существующий проект на Go с неподдерживаемым менеджером зависимостей.
  • Существующий проект на Go без какого-либо менеджера зависимостей.

Первый случай описан в Использование Go Modules; мы рассмотрим последние два в этой статье.

С использованием менеджера зависимостей

Чтобы преобразовать проект, который уже использует инструмент управления зависимостями, выполните следующие команды:

<code>$ git clone https://github.com/my/project
[...]
$ cd project
$ cat Godeps/Godeps.json
{
  "ImportPath": "github.com/my/project",
  "GoVersion": "go1.12",
  "GodepVersion": "v80",
  "Deps": [
  {
    "ImportPath": "rsc.io/binaryregexp",
    "Comment": "v0.2.0-1-g545cabd",
    "Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
  },
  {
    "ImportPath": "rsc.io/binaryregexp/syntax",
    "Comment": "v0.2.0-1-g545cabd",
    "Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
  }
  ]
}
$ go mod init github.com/my/project
go: creating new go.mod: module github.com/my/project
go: copying requirements from Godeps/Godeps.json
$ cat go.mod
module github.com/my/project
go 1.12
require rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$
</code>

go mod init создаёт новый файл go.mod и автоматически импортирует зависимости из Godeps.json, Gopkg.lock или ряда других поддерживаемых форматов. Аргумент команды go mod init — это путь модуля (module path), место, где можно найти модуль.

Это хорошее время, чтобы остановиться и выполнить go build ./... и go test ./... перед тем, как продолжить. Последующие шаги могут изменить ваш файл go.mod, поэтому если вы предпочитаете итеративный подход, то этот момент — ближайший к вашему предыдущему описанию зависимостей до появления модулей.

<code>$ go mod tidy
go: downloading rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
go: extracting rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$ cat go.sum
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca h1:FKXXXJ6G2bFoVe7hX3kEX6Izxw5ZKRH57DFBJmHCbkU=
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
$
</code>

go mod tidy находит все пакеты, транзитивно импортируемые пакетами в вашем модуле. Он добавляет новые требования к модулям для пакетов, которые не предоставляются никаким известным модулем, и удаляет требования к модулям, которые не предоставляют никаких импортированных пакетов. Если модуль предоставляет пакеты, которые импортируются только проектами, ещё не перешедшими на модули, то требование к модулю будет помечено комментарием // indirect. Всегда рекомендуется выполнять go mod tidy перед тем, как коммитить файл go.mod в систему контроля версий.

Давайте завершим, убедившись, что код собирается и тесты проходят:

<code>$ go build ./...
$ go test ./...
[...]
$
</code>

Обратите внимание, что другие менеджеры зависимостей могут указывать зависимости на уровне отдельных пакетов или целых репозиториев (не модулей), и обычно не учитывают требования, указанные в файлах go.mod зависимостей. Следовательно, вы можете не получить точно те же версии всех пакетов, что и раньше, и существует риск обновления до версий с ломающими изменениями. Поэтому важно после выполнения вышеуказанных команд провести аудит полученных зависимостей. Для этого выполните

<code>$ go list -m all
go: finding rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
github.com/my/project
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$
</code>

и сравните полученные версии с вашим старым файлом управления зависимостями, чтобы убедиться, что выбранные версии подходят. Если вы обнаружите версию, которая не соответствует вашим ожиданиям, вы можете выяснить причину с помощью go mod why -m и/или go mod graph, а затем обновить или понизить версию до нужной с помощью go get. (Если запрашиваемая вами версия старше ранее выбранной, go get понизит другие зависимости при необходимости, чтобы сохранить совместимость.) Например,

<code>$ go mod why -m rsc.io/binaryregexp
[...]
$ go mod graph | grep rsc.io/binaryregexp
[...]
$ go get rsc.io/binaryregexp@v0.2.0
$
</code>

Без менеджера зависимостей

Для проекта на Go без системы управления зависимостями, начните с создания файла go.mod:

<code>$ git clone https://go.googlesource.com/blog
[...]
$ cd blog
$ go mod init golang.org/x/blog
go: creating new go.mod: module golang.org/x/blog
$ cat go.mod
module golang.org/x/blog
go 1.12
$
</code>

Если конфигурационный файл от предыдущего менеджера зависимостей отсутствует, go mod init создаст файл go.mod только с директивами module и go. В этом примере мы установили путь модуля в golang.org/x/blog, потому что это пользовательский путь импорта. Пользователи могут импортировать пакеты с этим путем, и мы должны быть осторожны, чтобы не изменить его.

Директива module объявляет путь модуля, а директива go объявляет ожидаемую версию языка Go, используемую для компиляции кода внутри модуля.

Далее выполните go mod tidy для добавления зависимостей модуля:

<code>$ go mod tidy
go: finding golang.org/x/website latest
go: finding gopkg.in/tomb.v2 latest
go: finding golang.org/x/net latest
go: finding golang.org/x/tools latest
go: downloading github.com/gorilla/context v1.1.1
go: downloading golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: extracting github.com/gorilla/context v1.1.1
go: extracting golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: downloading gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
go: extracting golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
$ cat go.mod
module golang.org/x/blog
go 1.12
require (
  github.com/gorilla/context v1.1.1
  golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
  golang.org/x/text v0.3.2
  golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
  golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
  gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
)
$ cat go.sum
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
[...]
$
</code>

go mod tidy добавил требования модуля для всех пакетов, транзитивно импортированных пакетами в вашем модуле, и создал go.sum с контрольными суммами для каждой библиотеки в определённой версии. Завершим, убедившись, что код всё ещё собирается и тесты проходят:

<code>$ go build ./...
$ go test ./...
ok      golang.org/x/blog   0.335s
?       golang.org/x/blog/content/appengine [no test files]
ok      golang.org/x/blog/content/cover 0.040s
?       golang.org/x/blog/content/h2push/server [no test files]
?       golang.org/x/blog/content/survey2016    [no test files]
?       golang.org/x/blog/content/survey2017    [no test files]
?       golang.org/x/blog/support/racy  [no test files]
$
</code>

Обратите внимание, что когда go mod tidy добавляет зависимость, он добавляет последнюю версию модуля. Если ваш GOPATH включал более старую версию зависимости, которая затем опубликовала разрушающее изменение, вы можете увидеть ошибки в go mod tidy, go build или go test. Если это произошло, попробуйте понизить версию с помощью go get (например, go get github.com/broken/module@v1.1.0), или потратьте время, чтобы сделать ваш модуль совместимым с последней версией каждой зависимости.

Тесты в режиме модулей

Некоторые тесты могут потребовать доработки после перехода к использованию Go модулей.

Если тесту нужно записывать файлы в директорию пакета, он может завершиться неудачей, если директория пакета находится в кэше модулей, который доступен только для чтения. В частности, это может привести к сбою команды go test all. Вместо этого тест должен копировать необходимые файлы в временную директорию.

Если тест полагается на относительные пути (../package-in-another-module) для поиска и чтения файлов в другом пакете, он может завершиться неудачей, если пакет находится в другом модуле, который будет расположен в версионированной поддиректории кэша модулей или по пути, указанному в директиве replace. Если это так, возможно, потребуется скопировать входные данные теста в ваш модуль, или преобразовать входные данные теста из обычных файлов в данные, встроенные в исходные файлы .go.

Если тест ожидает, что команды go внутри теста будут выполняться в режиме GOPATH, он может завершиться неудачей. Если это так, возможно, потребуется добавить файл go.mod в дерево исходных кодов для тестирования, или явно установить GO111MODULE=off.

Публикация релиза

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

<code>$ git tag v1.2.0
$ git push origin v1.2.0
</code>

Ваш новый файл go.mod определяет канонический путь импорта для вашего модуля и добавляет новые минимальные требования к версиям. Если ваши пользователи уже используют правильный путь импорта, и ваши зависимости не вносили разрушающих изменений, то добавление файла go.mod является обратно совместимым — но это значительное изменение, и может выявить существующие проблемы. Если у вас уже есть теги версий, вы должны увеличить номер дополнительной версии. См. Публикация Go модулей, чтобы узнать, как увеличивать и публиковать версии.

Импорты и канонические пути модулей

Каждый модуль объявляет свой путь модуля в файле go.mod. Каждая инструкция import, ссылающаяся на пакет внутри модуля, должна иметь путь модуля в качестве префикса пути пакета. Однако команда go может столкнуться с репозиторием, содержащим модуль, через множество различных удалённых путей импорта. Например, как golang.org/x/lint, так и github.com/golang/lint разрешаются в репозитории, содержащий код, размещённый на go.googlesource.com/lint. Файл go.mod, содержащийся в этом репозитории, объявляет его путь как golang.org/x/lint, поэтому только этот путь соответствует действительному модулю.

Go 1.4 предоставила механизм объявления канонических путей импорта с использованием // import комментариев, но авторы пакетов не всегда их предоставляли. В результате код, написанный до появления модулей, мог использовать неканонический путь импорта для модуля, не вызывая ошибку из-за несоответствия. При использовании модулей путь импорта должен соответствовать каноническому пути модуля, поэтому может потребоваться обновить инструкции import: например, может потребоваться изменить import "github.com/golang/lint" на import "golang.org/x/lint".

Другая ситуация, в которой канонический путь модуля может отличаться от его пути репозитория, происходит с Go модулями, имеющими номер основной версии 2 или выше. Go модуль с номером основной версии выше 1 должен включать суффикс номера основной версии в свой путь модуля: например, версия v2.0.0 должна иметь суффикс /v2. Однако инструкции import могли ссылаться на пакеты внутри модуля без этого суффикса. Например, пользователи, не использующие модули, для github.com/russross/blackfriday/v2 в v2.0.1 могли импортировать его как github.com/russross/blackfriday, и им нужно будет обновить путь импорта, чтобы включить суффикс /v2.

Заключение

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

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

Спасибо за всю вашу обратную связь и помощь в улучшении модулей.

Следующая статья: Запущена копия модулей и база данных контрольных сумм
Предыдущая статья: Суммит участников 2019
Индекс блога

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang