Published on
Updated on 

Введение в модули Go

Authors
  • Эта публикация - перевод статьи. Ее автор - Roberto Selbach. Оригинал доступен по ссылке ниже:

    Introduction to Go 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 будет использовать теги репозитория при поиске версий, а некоторые версии отличаются от других: например, версии 2 и выше должны иметь путь импорта, отличный от версий 0 и 1 (мы вернемся к этому.) Кроме того, по умолчанию Go будет извлекать последнюю версию с тегами, доступную в репозитории. Это важный момент, так как вы можете привыкнуть работать с главной веткой. На данный момент вам нужно помнить, что для выпуска нашего пакета нам нужно пометить наш репозиторий версией. Итак, давайте сделаем это.

Делаем наш первый релиз

Теперь, когда наш пакет готов, мы можем выпустить его для всего мира. Мы делаем это с помощью тегов версий. Давайте выпустим нашу версию 1.0.0:

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

Это создает тег в моем репозитории Github, помечающий текущую фиксацию как версию 1.0.0. Но это никак не навязывается, но хорошей идеей является также создание новой ветви («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 и пометить его как новый выпуск.

    $ 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/[email protected])

В приведенном выше списке, похоже, нет способа обновления до последней  основной версии. Для этого есть веская причина, как мы увидим чуть позже. Так как наша программа использовала версию 1.0.0 нашего пакета, и мы только что создали версию 1.0.1,  любая из следующих команд обновит нас до 1.0.1:

    $ go get -u
    $ go get -u=patch
    $ go get github.com/robteix/[email protected]

После запуска, скажем, 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/ каталог по умолчанию. Идея состоит в том, чтобы в _конечном итоге_покончить с вендингом. Но если мы все еще хотим добавить зависимости от поставщиков в наш контроль версий, мы все равно можем это сделать:

    $ go mod vendor

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

    $ go build -mod vendor

Я ожидаю, что многие разработчики, желающие использовать вендоринг, будут go buildнормально работать на своих машинах разработки и использовать -mod vendor в своих CI. Опять же, модули Go отходят от идеи вендинга и переходят на использование прокси-модуля Go для тех, кто не хочет зависеть от прямое управление службами контроля версий. Существуют способы гарантировать, что goони вообще не достигнут сети (например GOPROXY=off), но это тема для будущего сообщения в блоге.

Заключение

Этот пост может показаться немного сложным, но я попытался объяснить многое вместе. Реальность такова, что теперь модули Go в основном прозрачны. Мы импортируем пакет как всегда в нашем коде, и goкоманда позаботится обо всем остальном. Когда мы что-то создадим, зависимости будут извлечены автоматически. Это также устраняет необходимость использования, $GOPATHкоторое было препятствием для новых разработчиков Go, которым было трудно понять, почему вещи должны идти в конкретный каталог. Вендорство (неофициально) не рекомендуется в пользу использования прокси.

Я могу сделать отдельный пост про прокси модуля Go. (Обновление в прямом эфире .)Я думаю, что это получилось слишком сильным, и у людей осталось впечатление, что в настоящее время вендор убирают. Это не так. Торговая деятельность все еще работает, хотя и немного отличается от прежней. Кажется, есть желание заменить вендор на что-то лучшее, что может быть или не быть доверенным лицом. Но на данный момент это всего лишь желание найти лучшее решение. Торговая деятельность не исчезнет, ​​пока не будет найдена хорошая замена (если вообще когда-либо).