Введение в систему модулей Go

в 12:00, , рубрики: Go, golang, modules

Грядущий релиз версии 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js