Управление пакетами с помощью модулей Go: Прагматическое руководство

Автор оригинала: Alexander Diachenko
  • Перевод
Всем привет. В преддверии старта курса «Разработчик Golang» подготовили для вас еще один интересный перевод.




Модули — это способ борьбы с зависимостями в Go. Изначально представленные в качестве эксперимента, модули предполагают вывести на поле в качестве нового стандарта для управления пакетами с версии 1.13.

Я нахожу эту тему достаточно необычной для новичков, пришедших с других языков, и поэтому я решил собрать здесь некоторые соображения и советы, чтобы помочь другим, таким же как я, получить представление об управлении пакетами в Go. Мы начнем с общего знакомства, а затем перейдем к менее очевидным аспектам, включая использование папки vendor, использование модулей с Docker в разработке, зависимости инструментов и т. д.

Если вы уже знакомы с модулями Go и знаете Wiki, как свои пять пальцев, эта статья, вероятно, не будет для вас очень полезной. Но для остальных, однако, она может сэкономить несколько часов проб и ошибок.

Так что если вам по пути, запрыгивайте и наслаждайтесь поездкой.



Быстрый запуск


Если в ваш проект уже интегрировано управление версиями, вы можете просто запустить

go mod init

Или указать путь к модулю вручную. Это что-то вроде имени, URL и пути импорта для вашего пакета:

go mod init github.com/you/hello

Эта команда создаст файл go.mod, который одновременно определяет требования проекта и лочит зависимости на их правильные версии (в качестве аналогии для вас, это как package.json и package-lock.json, объединенные в один файл):

module github.com/you/hello
go 1.12

Запустите go get, чтобы добавить новую зависимость в ваш проект:

Обратите внимание, что хотя вы не можете указать диапазон версий с помощью go get, то что вы здесь определяете, это не конкретная, а минимальная версия. Как мы увидим позже, есть способ изящно актуализировать зависимости в соответствии с semver.

# use Git tags
go get github.com/go-chi/chi@v4.0.1
# or Git branch name
go get github.com/go-chi/chi@master
# or Git commit hash
go get github.com/go-chi/chi@08c92af

Теперь наш файл go.mod выглядит следующим образом:

module github.com/you/hello
go 1.12
require github.com/go-chi/chi v4.0.2+incompatible // indirect

Суффикс +incompatible добавляется ко всем пакетам, которые еще не настроены под модули Go или нарушают их правила управления версиями.

Поскольку мы еще нигде в нашем проекте не импортировали этот пакет, он был помечен как // indirect. Мы можем привести это в порядок с помощью следующей команды:

go mod tidy

В зависимости от текущего состояния вашего репозитория, она либо удалит неиспользуемый модуль, либо удалит комментарий // indirect.

Если какая-либо зависимость сама по себе не имеет go.mod (например, она еще не настроена под модули), тогда все ее зависимости будут записаны в родительский файл go.mod (как вариант, ваш файл go.mod) вместе с комментарием // indirect, чтобы указать, что они там не от прямого импорта в ваш модуль.

В глобальном плане цель go mod tidy состоит также в добавлении любых зависимостей, необходимых для других комбинаций ОС, архитектур и тегов сборки. Обязательно запускайте ее перед каждым релизом.

Следите также за тем, чтобы после добавления зависимости был создан файл go.sum. Вам может показаться, что это lock-файл. Но на самом деле go.mod уже предоставляет достаточно информации для на 100% воспроизводимых сборок. Файл go.sum создается в проверочных целях: он содержит ожидаемые криптографические контрольные суммы содержимого отдельных версий модуля.

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


Только что мигрировали mkcert в модули (с vendor/ для обратной совместимости) и все прошло гладко
https://github.com/FiloSottile/mkcert/commit/26ac5f35395fb9cba3805faf1a5a04d260271291

$ GO111MODULE=on go1.11rc1 mod init
$ GO111MODULE=on go1.11rc1 mod vendor
$ git add go.mod go.sum vendor
$ git rm Gopkg.lock Gopkg.toml Makefile



FAQ: Должен ли я коммитить go.sum в git?
A: Определенно да. С ним обладателям ваших источников не нужно доверять другим репозиториям GitHub и владельцам пользовательских путей импорта. Уже на пути к нам нечто получше, ну а пока это та же модель, что и хэши в lock-файлах.

Команды go build и go test, автоматически загрузят все отсутствующие зависимости, хотя вы можете сделать это явно с помощью go mod download, чтобы предварительно заполнить локальные кэши, которые могут оказаться полезными для CI.

По умолчанию все наши пакеты из всех проектов загружаются в каталог $GOPATH/pkg/mod. Мы обсудим это подробнее позже.

Обновление версий пакетов


Вы можете использовать go get -u или go get -u=patch для обновления зависимостей до последней минорной версии или патча соответственно.

Но вы не можете обновиться так до мажорных версий. Код, включаемый в модули Go, должен технически соответствовать следующим правилам:

  • Соответствовать semver (пример тега VCS v1.2.3).
  • Если модуль версии v2 или выше, мажорная версия модуля должна быть включена как /vN в конце пути модуля, используемого в файле go.mod, и в пути импорта пакета:

import "github.com/you/hello/v2"

По-видимому, это сделано для того, чтобы разные версии пакетов могли быть импортированы в одной сборке (см. diamond dependency problem).

В двух словах, Go ожидает, что вы будете очень осмотрительны при внесении мажорных версий.

Замена импортированных модулей


Вы можете указать необходимый модуль для своего собственного форка или даже локального пути к файлу, используя директиву replace:

go mod edit -replace github.com/go-chi/chi=./packages/chi

Результат:

module github.com/you/hello
go 1.12
require github.com/go-chi/chi v4.0.2+incompatible
replace github.com/go-chi/chi => ./packages/chi

Вы можете удалить строку вручную или запустить:

go mod edit -dropreplace github.com/go-chi/chi

Попроектное управление зависимостями


Исторически весь код Go хранился в одном гигантском монорепозитории, потому что именно так Google организовывает свою кодовую базу, и это сказывается на дизайне языка.

Модули Go — это своего рода отступление от этого подхода. Вам больше не нужно хранить все свои проекты в $GOPATH.

Тем не менее, технически все ваши загруженные зависимости все еще помещаются в $GOPATH/pkg/mod. Если вы используете Docker-контейнеры при локальной разработке, это может стать проблемой, поскольку зависимости хранятся вне проекта. По умолчанию они просто не видны в вашей IDE.



Обычно это не проблема для других языков, но это то, с чем я впервые столкнулся при работе с кодовой базой Go.

К счастью, есть несколько (недокументированных) способов решения этой проблемы.

Вариант 1. Установите GOPATH внутри каталога вашего проекта.


На первый взгляд это может показаться нелогичным, но если вы запускаете Go из контейнера, вы можете переопределить его GOPATH, чтобы он указывал на каталог проекта для того, чтобы пакеты были доступны из хоста:

version: '3.7'

services:
  app:
    command: tail -f /dev/null
    image: golang:1.12.6-stretch
    environment:
      # Все ваши зависимости будут расположены прямо здесь - /code/.go/pkg/mod
      - GOPATH=/code/.go
    ports:
      - 8000:8000
    volumes:
      - ./:/code:cached
    working_dir: /code

Популярные IDE должны иметь возможность установить GOPATH на уровне проекта (рабочей области):



Единственный недостаток этого подхода — отсутствие взаимодействия со средой выполнения Go на хост-компьютере. Вы должны выполнять все команды Go внутри контейнера.

Вариант 2: Вендоринг ваших зависимостей


Еще один способ — скопировать зависимости вашего проекта в папку vendor:

go mod vendor

Следует сразу отметить: мы НЕ разрешаем Go прямую загрузку материалов в папку vendor: с модулями это невозможно. Мы просто копируем уже загруженные пакеты.

К тому же, если вы отвендорите свои зависимости, как в примере выше, затем очистите $GOPATH/pkg/mod, а затем попробуйте добавить несколько новых зависимостей в ваш проект, вы увидите следующее:

  1. Go перестроит кэш загрузки для всех пакетов по $GOPATH/pkg/mod/cache.
  2. Все загруженные модули будут скопированы в $GOPATH/pkg/mod.
  3. И, наконец, Go скопирует эти модули в vendor папку, удаляя примеры, тесты и некоторые другие файлы, от которых вы напрямую не зависите.

Более того, в этой недавно созданной vendor-папке отсутствует много вещей:



Типичный файл Docker Compose выглядит следующим образом (обратите внимание на привязки томов):

version: '3.7'

services:
  app:
    command: tail -f /dev/null
    image: golang:1.12.6-stretch
    ports:
      - 8000:8000
    volumes:
     # Это кэш модулей go, без него вам придется повторно загружать все зависимости после перезапуска контейнера
      - modules:/go/pkg/mod/cache
      - ./:/code:cached
    working_dir: /code 

volumes:
  modules:
    driver: local

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

Однако, когда я читаю комментарии от некоторых мейнтейнеров Go и некотроые предложения, связанные с частичным вендорингом (ЧЕ?), у меня складывается впечатление, что изначально эта фича предназначалась не для этого юзкейса.

Один из комментаторов на reddit помог мне пролить свет на это:

Обычно люди вендорят свои зависимости по таким причинам, как желание иметь герметичные сборки без доступа к сети, а также наличия копии готовых зависимостей в случае отказа github или исчезновения репозитория, и возможность более легкого аудита изменений в зависимостях с использованием стандартных инструментов VCS и т. д.

Да, не похоже на что-либо из того, что может меня заинтересовать.

Согласно команде Go, вы можете запросто подключить вендоринг, установив переменную среды GOFLAGS=-mod=vendor. Я не рекомендую так делать. Использование флагов просто сломает go get без предоставления каких-либо других преимуществ для вашего ежедневного рабочего процесса:



На самом деле, единственное место где вам нужно подключить вендоринг — это ваше IDE:



После нескольких проб и ошибок я пришел к следующей процедуре для добавления вендорных зависимостей в этом подходе.

Шаг 1. Требование


Вы можете потребовать зависимость с помощью go get:

go get github.com/rs/zerolog@v1.14.3

Шаг 2. Импорт


Затем импортируйте его куда-нибудь в своем коде:

import (
   _ "github.com/rs/zerolog"
)

Шаг 3. Вендоринг


Наконец, отвендорите ваши зависимости заново:

go mod vendor

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

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

Этот подход лучше взаимодействует с хост-системой, но он довольно запутан, когда дело доходит до редактирования ваших зависимостей.



Лично я думаю, что переопределение GOPATH является более чистым подходом, поскольку он не жертвует функциональность go get. Тем не менее, я хотел показать обе стратегии, потому что папка vendor может быть привычнее для людей, пришедших с других языков, таких как PHP, Ruby, Javascript и т. д. Как вы можете увидеть из махинаций, описанных в этой статье, это не особенно хороший выбор для Go.

Зависимости инструментов


Нам может понадобиться установить некоторые инструменты на основе Go, которые не импортируются, а используются как часть среды разработки проекта. Простым примером такого инструмента является CompileDaemon, который может наблюдать за вашим кодом на предмет изменений и перезапускать ваше приложение.

Официально рекомендуемый подход заключается в добавлении tools.go файла(имя не имеет значения) со следующим содержанием:

// +build tools
package tools
import (
_ "github.com/githubnemo/CompileDaemon"
)

  • Ограничение // +build tools не позволяет вашим обычным сборкам фактически импортировать ваш инструмент.
  • Выражение import позволяет командам go точно записывать информацию о версии ваших инструментов в файл go.mod вашего модуля.

Ну вот и все. Я надеюсь, что вы не будете так же озадачены, как я, когда я впервые начал использовать модули Go. Вы можете посетить Go Modules wiki для получения более подробной информации.



Успеть на курс.


OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 0

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

Самое читаемое