The Go Blog

Использование Go Modules

Tyler Bui-Palsulich и Eno Compton
19 марта 2019

Введение

Эта запись является частью серии.

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

Go 1.11 и 1.12 включают предварительную поддержку модулей, новую систему управления зависимостями Go, которая делает информацию о версиях зависимостей явной и облегчает её управление. Эта статья представляет собой введение в базовые операции, необходимые для начала работы с модулями.

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

Начиная с Go 1.11, команда go включает использование модулей при наличии файла go.mod в текущем каталоге или любом родительском каталоге, при условии, что каталог находится вне $GOPATH/src. (Внутри $GOPATH/src, для совместимости, команда go всё ещё работает в старом режиме GOPATH, даже если файл go.mod найден. См. подробности в документации команды go.) Начиная с Go 1.13, режим модулей станет стандартным для всего разработки.

В этой записи рассматриваются последовательность общих операций, которые возникают при разработке Go кода с использованием модулей:

  • Создание нового модуля.
  • Добавление зависимости.
  • Обновление зависимостей.
  • Добавление зависимости на новую основную версию.
  • Обновление зависимости до новой основной версии.
  • Удаление неиспользуемых зависимостей.

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

Создадим новый модуль.

Создайте новый, пустой каталог где-либо вне $GOPATH/src, перейдите в этот каталог с помощью cd, а затем создайте новый исходный файл, hello.go:

<code>package hello
func Hello() string {
  return "Hello, world."
}
</code>

Также напишем тест в файле hello_test.go:

<code>package hello
import "testing"
func TestHello(t *testing.T) {
  want := "Hello, world."
  if got := Hello(); got != want {
    t.Errorf("Hello() = %q, want %q", got, want)
  }
}
</code>

На данном этапе в директории содержится пакет, но не модуль, поскольку отсутствует файл go.mod. Если бы мы работали в директории /home/gopher/hello и выполнили команду go test, то получили бы следующий вывод:

<code>$ go test
PASS
ok      _/home/gopher/hello 0.020s
$
</code>

Последняя строка представляет собой сводку по общему тестированию пакета. Поскольку мы работаем вне $GOPATH и также вне любого модуля, команда go не знает импортного пути для текущей директории и создаёт фиктивный путь, основанный на имени директории: _/home/gopher/hello.

Сделаем текущую директорию корневой для модуля, используя команду go mod init, а затем снова попробуем выполнить go test:

<code>$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok      example.com/hello   0.020s
$
</code>

Поздравляем! Вы написали и протестировали свой первый модуль.

Команда go mod init создала файл go.mod:

<code>$ cat go.mod
module example.com/hello
go 1.12
$
</code>

Файл go.mod появляется только в корне модуля. Пакеты в поддиректориях имеют импортные пути, состоящие из пути модуля плюс путь к поддиректории. Например, если мы создадим поддиректорию world, то не нужно (и не стоит) запускать go mod init там. Пакет будет автоматически распознан как часть модуля example.com/hello с импортным путём example.com/hello/world.

Добавление зависимости

Основной мотивацией для создания Go модулей была улучшенная поддержка использования (то есть добавления зависимости на) код, написанный другими разработчиками.

Обновим наш файл hello.go, чтобы импортировать rsc.io/quote и использовать его для реализации функции Hello:

<code>package hello
import "rsc.io/quote"
func Hello() string {
  return quote.Hello()
}
</code>

Теперь снова запустим тест:

<code>$ go test
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok      example.com/hello   0.023s
$
</code>

Команда go разрешает импорты, используя конкретные версии зависимостей, перечисленные в go.mod. Когда она сталкивается с импортом пакета, который не предоставляется никаким модулем в go.mod, команда go автоматически ищет модуль, содержащий этот пакет, и добавляет его в go.mod, используя последнюю версию. («Последняя» определяется как последняя тегированная стабильная (не-предрелизная) версия, либо последняя тегированная предрелизная версия, либо последняя нетегированная версия.) В нашем примере go test разрешил новый импорт rsc.io/quote к модулю rsc.io/quote v1.5.2. Он также загрузил две зависимости, используемые rsc.io/quote, а именно rsc.io/sampler и golang.org/x/text. В файле go.mod записываются только прямые зависимости:

<code>$ cat go.mod
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2
$
</code>

Вторая команда go test не будет повторять эту работу, поскольку go.mod теперь актуален, а загруженные модули кэшируются локально (в $GOPATH/pkg/mod):

<code>$ go test
PASS
ok      example.com/hello   0.020s
$
</code>

Обратите внимание, что хотя команда go делает добавление нового зависимости быстрым и простым, это не бесплатно. Ваш модуль теперь буквально зависит от новой зависимости в критических аспектах, таких как корректность, безопасность и правильная лицензия, чтобы назвать лишь несколько из них. Для дополнительных соображений см. запись в блоге Расса Кокса, “Our Software Dependency Problem.”

Как мы видели выше, добавление одной прямой зависимости часто приводит к появлению других косвенных зависимостей. Команда go list -m all выводит текущий модуль и все его зависимости:

<code>$ go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$
</code>

В выводе команды go list, текущий модуль, также известный как главный модуль, всегда находится на первой строке, за которым следуют зависимости, отсортированные по пути модуля.

Версия golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c является примером псевдо-версии, которая представляет собой синтаксис версий команды go для конкретного непомеченного коммита.

Помимо go.mod, команда go поддерживает файл с именем go.sum, содержащий ожидаемые криптографические хеши содержимого конкретных версий модулей:

<code>$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
$
</code>

Команда go использует файл go.sum для того, чтобы убедиться, что будущие загрузки этих модулей получают те же данные, что и при первой загрузке, чтобы гарантировать, что модули, от которых зависит ваш проект, не изменяются неожиданно, независимо от причин: злонамеренных, случайных или других. И go.mod, и go.sum должны быть добавлены в систему контроля версий.

Обновление зависимостей

С использованием модулей Go версии указываются с помощью тегов семантического версионирования. Семантическая версия имеет три части: основная, второстепенная и патч. Например, для v0.1.2 основная версия — 0, второстепенная — 1, а патч — 2. Рассмотрим несколько обновлений второстепенных версий. В следующем разделе мы рассмотрим обновление основной версии.

Из вывода команды go list -m all мы можем увидеть, что используется непомеченная версия пакета golang.org/x/text. Давайте обновим её до последней помеченной версии и проверим, что всё ещё работает:

<code>$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok      example.com/hello   0.013s
$
</code>

Ура! Все тесты проходят. Давайте ещё раз взглянем на вывод команды go list -m all и файл go.mod:

<code>$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello
go 1.12
require (
  golang.org/x/text v0.3.0 // indirect
  rsc.io/quote v1.5.2
)
$
</code>

Пакет golang.org/x/text был обновлён до последней помеченной версии (v0.3.0). Файл go.mod также был обновлён, чтобы указать версию v0.3.0. Комментарий indirect показывает, что зависимость не используется напрямую этим модулем, а только косвенно другими зависимостями модулей. См. go help modules для получения подробной информации.

Теперь давайте попробуем обновить минорную версию rsc.io/sampler. Начнём так же, как и раньше — запустив go get и выполнив тесты:

<code>$ go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99
$ go test
--- FAIL: TestHello (0.00s)
hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
exit status 1
FAIL    example.com/hello   0.014s
$
</code>

Ой! Ошибка теста показывает, что последняя версия rsc.io/sampler несовместима с нашим использованием. Давайте выведем доступные помеченные версии этого модуля:

<code>$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
$
</code>

Мы использовали версию v1.3.0; очевидно, что v1.99.99 нам не подходит. Может быть, стоит попробовать использовать v1.3.1:

<code>$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok      example.com/hello   0.022s
$
</code>

Обратите внимание на явное указание @v1.3.1 в аргументе команды go get. В общем случае каждый аргумент, передаваемый в go get, может содержать явную версию; по умолчанию используется @latest, которая разрешается в последнюю версию, определённую ранее.

Добавление зависимости на новую мажорную версию

Добавим новую функцию в наш пакет: func Proverb возвращает пословицу о параллелизме на Go, вызывая quote.Concurrency, которая предоставляется модулем rsc.io/quote/v3. Сначала мы обновим hello.go, чтобы добавить новую функцию:

<code>package hello
import (
  "rsc.io/quote"
  quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
  return quote.Hello()
}
func Proverb() string {
  return quoteV3.Concurrency()
}
</code>

Затем мы добавляем тест в файл hello_test.go:

<code>func TestProverb(t *testing.T) {
  want := "Concurrency is not parallelism."
  if got := Proverb(); got != want {
    t.Errorf("Proverb() = %q, want %q", got, want)
  }
}
</code>

Затем можно протестировать код:

<code>$ go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok      example.com/hello   0.024s
$
</code>

Обратите внимание, что теперь наш модуль зависит от обоих пакетов: rsc.io/quote и rsc.io/quote/v3:

<code>$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$
</code>

Каждая отдельная мажорная версия (например, v1, v2 и так далее) Go модуля использует другой путь модуля: начиная с версии v2, путь должен заканчиваться на номер мажорной версии. В примере, версия v3 пакета rsc.io/quote больше не называется просто rsc.io/quote: вместо этого она идентифицируется по пути модуля rsc.io/quote/v3. Эта конвенция называется семантическое версионирование импортов, и она даёт несовместимым пакетам (те, у которых разные мажорные версии) разные имена. Напротив, версия v1.6.0 пакета rsc.io/quote должна быть обратно совместима с версией v1.5.2, поэтому она переиспользует имя rsc.io/quote. (В предыдущем разделе, rsc.io/sampler v1.99.99 должен был быть обратно совместим с rsc.io/sampler v1.3.0, но ошибки или неправильные предположения клиентов о поведении модуля могут возникнуть.)

Команда go позволяет включать в сборку не более одной версии любого конкретного пути модуля, то есть не более одной мажорной версии: один rsc.io/quote, один rsc.io/quote/v2, один rsc.io/quote/v3, и так далее. Это даёт авторам модулей чёткое правило относительно возможного дублирования пути одного и того же модуля: невозможно собрать программу, использующую одновременно rsc.io/quote v1.5.2 и rsc.io/quote v1.6.0. Одновременно разрешение использования разных мажорных версий модуля (поскольку у них разные пути) даёт потребителям модулей возможность постепенно переходить на новую мажорную версию. В этом примере мы хотели использовать quote.Concurrency из rsc/quote/v3 v3.1.0 но ещё не готовы полностью перейти на использование rsc.io/quote v1.5.2. Возможность постепенного перехода особенно важна в крупных программах или кодовых базах.

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

Давайте завершим преобразование, чтобы использовать только rsc.io/quote/v3, вместо rsc.io/quote. Из-за изменения мажорной версии, можно ожидать, что некоторые API могли быть удалены, переименованы или иным образом изменены несовместимым образом. Просмотрев документацию, можно увидеть, что функция Hello была переименована в HelloV3:

<code>$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote/v3"
Пакет quote собирает короткие высказывания.
func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$
</code>

Можно обновить использование quote.Hello() в hello.go, чтобы использовать quoteV3.HelloV3():

<code>package hello
import quoteV3 "rsc.io/quote/v3"
func Hello() string {
  return quoteV3.HelloV3()
}
func Proverb() string {
  return quoteV3.Concurrency()
}
</code>

А затем в этот момент уже нет необходимости в переименованном импорте, поэтому можно отменить его:

<code>package hello
import "rsc.io/quote/v3"
func Hello() string {
  return quote.HelloV3()
}
func Proverb() string {
  return quote.Concurrency()
}
</code>

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

<code>$ go test
PASS
ok      example.com/hello       0.014s
</code>

Удаление неиспользуемых зависимостей

Мы удалили все наши использования rsc.io/quote, но оно всё ещё отображается в go list -m all и в файле go.mod:

<code>$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello
go 1.12
require (
  golang.org/x/text v0.3.0 // indirect
  rsc.io/quote v1.5.2
  rsc.io/quote/v3 v3.0.0
  rsc.io/sampler v1.3.1 // indirect
)
$
</code>

Почему? Потому что сборка отдельного пакета, как это делается с помощью go build или go test, может легко определить, что чего-то не хватает и нужно добавить, но не может определить, что можно безопасно удалить. Удаление зависимости возможно только после проверки всех пакетов модуля, а также всех возможных комбинаций build-тегов для этих пакетов. Обычная команда сборки не загружает эту информацию, и поэтому не может безопасно удалять зависимости.

Команда go mod tidy очищает неиспользуемые зависимости:

<code>$ go mod tidy
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello
go 1.12
require (
  golang.org/x/text v0.3.0 // indirect
  rsc.io/quote/v3 v3.1.0
  rsc.io/sampler v1.3.1 // indirect
)
$ go test
PASS
ok      example.com/hello   0.020s
$
</code>

Заключение

Модули Go — это будущее управления зависимостями в Go. Функциональность модулей теперь доступна во всех поддерживаемых версиях Go (то есть в Go 1.11 и Go 1.12).

В этой статье были представлены следующие рабочие процессы с использованием Go модулей:

Мы призываем вас начать использовать модули в локальной разработке и добавлять файлы go.mod и go.sum в ваши проекты. Чтобы предоставить обратную связь и помочь сформировать будущее управления зависимостями в Go, просим вас отправлять нам сообщения об ошибках или отчеты об опыте.

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

Следующая статья: Отладка того, что вы развертываете в Go 1.12
Предыдущая статья: Новая сеть разработчиков Go
Индекс блога

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

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