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

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

Введение

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

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

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

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

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

Давайте создадим новый модуль.

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

package hello
func Hello() string {
    return "Hello, world."
}

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

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)
    }
}

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

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

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

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

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

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

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

$ cat go.mod
module example.com/hello

go 1.12
$

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

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

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

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

package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()
}

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

$ 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
$

Команда 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 записываются только прямые зависимости:

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote v1.5.2
$

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

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

Обратите внимание, что хотя команда go делает добавление новой зависимости быстрым и простым, она не обходится без затрат. Ваш модуль теперь буквально зависит от новой зависимости в критических областях, таких как корректность, безопасность и правильное лицензирование, и это лишь некоторые из них. Дополнительные сведения см. В блоге Расс Кокса «Проблема зависимости от программного обеспечения».

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

$ 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
$

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

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

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

$ 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...
$

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

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

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

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

$ 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
$

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

$ 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
)
$

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

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

$ 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
$

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

$ 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
$

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

$ go get rsc.io/[email protected]
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
$

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

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

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

package hello

import (
    "rsc.io/quote"
    quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
    return quote.Hello()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

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

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

Тогда мы можем проверить наш код:

$ 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
$

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

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

Каждая отдельная старшая версия (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 к использованию только rsc.io/quote/v3. Из-за существенного изменения версии следует ожидать, что некоторые API-интерфейсы могут быть удалены, переименованы или иным образом изменены несовместимыми способами. Читая документы, мы видим, что Hello стала HelloV3:

$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote"

Package quote collects pithy sayings.

func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$

(Обратите внимание на баг в выводе; отображаемый путь импорта ошибочно отбросил /v3, должно быть rsc.io/quote/v3.)

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

package hello

import quoteV3 "rsc.io/quote/v3"

func Hello() string {
    return quoteV3.HelloV3()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

И затем на этом этапе больше нет необходимости в переименованном импорте, поэтому мы можем отменить это:

package hello

import "rsc.io/quote/v3"

func Hello() string {
    return quote.HelloV3()
}

func Proverb() string {
    return quote.Concurrency()
}

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

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

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

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

$ 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
)
$

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

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

$ 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
$

Заключение

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

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

  • go mod init создает новый модуль, инициализируя файл go.mod, который его описывает.
  • go build, go test и другие команды построения пакетов добавляют новые зависимости в go.mod по мере необходимости.
  • go list -m all печатает зависимости текущего модуля.
  • go get изменяет требуемую версию зависимости (или добавляет новую зависимость).
  • go mod tidy удаляет неиспользуемые зависимости.

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

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

Тайлер Буй-Палсулич (Tyler Bui-Palsulic) и Ино Комптон (Eno Compton)

Leave a comment