[Перевод] Go += управление версиями пакетов
Статья написана в феврале 2018 года
В Go необходимо добавить версионирование пакетов.
Точнее, нужно добавить концепцию версионирования в рабочий словарь разработчиков Go и в инструменты, чтобы все употребляли одинаковые номера версий при упоминании, какую именно программу собрать, запустить или проанализировать. Команда go
должна точно говорить, какие версии каких пакетов находятся в конкретной сборке.
Нумерация версий позволяет сделать воспроизводимые сборки: если я выложу последнюю версию своей программы, вы получите не только последнюю версию моего кода, но и точно такие же версии всех пакетов, от которых зависит мой код, так что мы с вами создадим полностью эквивалентные двоичные файлы.
Версионирование также гарантирует, что завтра программа соберётся точно так же, как сегодня. Даже если вышли новые версии зависимостей, go
не станет их использовать без специальной команды.
Хотя нужно добавить управление версиями, не следует отказываться от главных преимуществ команды go
: это простота, скорость и понятность. Сегодня многие программисты не обращают внимания на версии, и всё работает нормально. Если сделать правильную модель, то программисты по-прежнему не будут обращать внимания на номера версий, просто всё станет лучше работать и станет понятнее. Существующие рабочие процессы практически не изменятся. Выпуск новых версий очень простой. В общем, управление версиями должно уйти на второй план и не отнимать внимание разработчика.
Короче говоря, нужно добавить управление версиями пакета, но не сломать go get
. В этой статье мы предлагаем, как это сделать, а также демонстрируем прототип, который вы можете попробовать уже сейчас, и который, надеюсь, станет основой для возможной интеграции go
. Надеюсь, статья станет началом продуктивной дискуссии о том, что работает, а что нет. На основе этого обсуждения я внесу коррективы как в своё предложение, так и в прототип, а затем представлю официальное предложение для добавления опциональной функции в Go 1.11.
Это предложение сохраняет все преимущества go get
, но добавляет воспроизводимые сборки, поддерживает семантическое управление версиями, устраняет вендоринг, убирает GOPATH в пользу рабочего процесса на основе проекта и обеспечивает плавный уход от dep
и его предшественников. Тем не менее, это предложение все ещё на ранней стадии. Если детали не верны, мы их исправим, прежде чем работа попадёт в основной дистрибутив Go.
Прежде чем изучить предложение, давайте посмотрим на текущее положение и как мы в нём оказались. Может, этот раздел слегка великоват, но история несёт важные уроки и помогает понять, почему мы хотим что-то изменить. Если это вам не интересно, можете сразу перейти к предложению или прочитать сопроводительную статью в блоге с примером.
Makefile
, goinstall
и go get
В ноябре 2009 года с первоначальной версией Go вышел компилятор, компоновщик и несколько библиотек. Чтобы скомпилировать и связать программы, требовалось запустить 6g
и 6l
, и мы включили в комплект примеры файлов makefile. Минимальная оболочка gobuild
могла собрать один пакет и написать соответствующий makefile (в большинстве случаев). Не было никакого установленного способа поделиться кодом с другими. Мы знали, что этого мало —, но выпустили то, что было, планируя разработать остальное совместно с сообществом.
В феврале 2010 года мы предложили goinstall, простую команду для загрузки пакетов из репозиториев систем управления версиями, таких как Bitbucket и GitHub. Goinstall
ввёл конвенции по путям импорта, которые сейчас считаются общепринятыми. Но в то время ни один код не следовал этим соглашениям, goinstall
сначала работал только с пакетами, которые не импортировали ничего, кроме стандартной библиотеки. Но разработчики быстро перешли к единому соглашению, которое мы знаем сегодня, и набор опубликованных пакетов Go вырос в целостную экосистему.
Goinstall также устранил файлы Makefile, а с ними и сложность пользовательских вариантов сборки. Хотя иногда бывает неудобно, что авторы пакетов не могут генерировать код во время каждой сборки, это упрощение невероятно важно для пользователей пакета: им не нужно беспокоиться об установке того же набора инструментов, который использовал автор. Такое упрощение также имеет решающее значение для работы инструментов. Makefile — это обязательный пошаговый рецепт компиляции пакета;, а применение к тому же пакету другого инструмента вроде go vet
или автозавершения может оказаться довольно сложным. Даже правильное получение зависимостей, чтобы собирать заново пакеты при необходимости и только при необходимости, намного сложнее с произвольными файлами Makefile. Хотя в то время некоторые возражали, что их лишают гибкости, но оглядываясь назад, становится ясно, что отказ от Makefile стал правильным шагом: выгоды намного перевешивают неудобства.
В декабре 2011 года в рамках подготовки Go 1 мы представили команду go, которая заменила goinstall
на go get
.
В целом, go get
представил значительные изменения: он позволил разработчикам Go обмениваться исходным кодом и использовать работы друг друга. Он также изолировал детали системы сборки внутри команды go
, так что стала возможной значительная автоматизация с помощью инструментария. Но go get
не хватает концепции управления версиями. В самых первых обсуждениях goinstall стало понятно: нужно что-то делать с управлением версиями. К сожалению, не было понятно, что именно делать. По крайней мере, мы в команде Go этого ясно не понимали. Когда go get
запрашивает пакет, то всегда получает последнюю копию, делегируя операции загрузки и обновления системе управления версиями, такой как Git или Mercurial. Такая «работа вслепую» привела по крайней мере к двум существенным недостаткам.
Управление версиями и стабильность API
Первый существенный недостаток go get
заключается в том, что без концепции управления версиями она не может ничего сказать пользователю о том, каких изменений ожидать в данном обновлении.
В ноябре 2013 года в версии Go 1.2 добавлена запись FAQ с таким советом относительно версионирования (текст не изменился к версии Go 1.10):
Пакеты для общего пользования должны поддерживать обратную совместимость по мере своего развития. Здесь уместны рекомендации по совместимости Go 1: не удаляйте экспортированные имена, поощряйте тегирование составных литералов и так далее. Если требуется новая функциональность, добавьте новое имя, а не меняйте старое. В случае кардинального изменения создайте новый пакет с новым путём импорта.
В марте 2014 года Густаво Нимейер запустил gopkg.in под вывеской «стабильные API для языка Go». Данный домен — GitHub-редирект с учётом версии, позволяющий импортировать пути вроде gopkg.in/yaml.v1
и gopkg.in/yaml.v2
для различных коммитов (возможно, в разных ветвях) одного репозитория Git. Согласно семантическому управлению версиями авторы должны при внесении критических изменений выпускать новую основную версию. Таким образом, более поздние версии пути импорта v1
заменяют предыдущие, а v2
может отдавать совершенно другие API.
В августе 2015 года Дэйв Чейни подал предложение о семантическом управлении версиями. В течение следующих нескольких месяцев это вызвало интересную дискуссию: казалось, все согласились, что семантическая пометка версий — прекрасная идея, но никто не знал, как инструменты должны работать с этими версиями.
Любые аргументы за семантическое версионирование неизбежно встречает критику со ссылкой на закон Хайрама:
Контракт вашего API становится не важен при достаточном количестве пользователей. От любого наблюдаемого поведения системы кто-то зависит.
Хотя закон Хайрама эмпирически верен, семантическое управление версиями по-прежнему является полезным способом формирования ожиданий по отношениям между релизами. Обновление с 1.2.3 до 1.2.4 не должно ломать ваш код, а обновление с 1.2.3 до 2.0.0 вполне может. Если код перестаёт работать после обновления на 1.2.4, то автор, скорее всего, примет баг-репорт и исправит ошибку в версии 1.2.5. Если код перестал работать (или даже компилироваться) после обновления на 2.0.0, то это изменение с гораздо большей вероятностью было преднамеренным и, соответственно, вряд ли что-то исправят в 2.0.1.
Я не хочу делать из закона Хайрама вывод, что семантическое версионирование невозможно. Вместо этого я считаю, что сборки следует использовать осторожно, используя точно такие же версии каждой зависимости, что и автор. То есть сборки по умолчанию должны быть максимально воспроизводимыми.
Вендоринг и воспроизводимые сборки
Второй существенный недостаток go get
заключается в том, что без концепции управления версиями команда не может обеспечить и даже выразить идею воспроизводимой сборки. Невозможно быть уверенным, что пользователи компилируют те же версии зависимостей кода, что и вы. В ноябре 2013 года в FAQ для Go 1.2 добавили ещё и такой совет:
Если вы используете внешний пакет и опасаетесь, что он может неожиданно измениться, самое простое решение — скопировать его в локальный репозиторий (такой подход используется в Google). Сохраните копию c новым путём импорта, который идентифицирует её как локальную копию. Например, можно скопировать
original.com/pkg
вyou.com/external/original.com/pkg
. Один из инструментов для этой процедуры —goven
Кита Рэрика.
Кит Рэрик начал этот проект в марте 2012 года. Утилита goven
копирует зависимость в локальный репозиторий и обновляет все пути импорта, чтобы отразить новое местоположение. Такие изменения исходного кода необходимы, но неприятны. Они затрудняют сравнение и включение новых копий, а также требуют обновления другого скопированного кода с использованием этой зависимости.
В сентябре 2013 года Кит представил godep, «новый инструмент для фиксации зависимостей пакетов». Основным достижением godep
стало то, что мы теперь называем вендорингом, то есть копирование зависимостей в проект без изменения исходных файлов, без прямой поддержки инструментов, путём определённой настройки GOPATH.
В октябре 2014 года Кит предложил добавить в инструменты Go поддержку «внешних пакетов», чтобы инструменты лучше понимали проекты, использующие эту конвенцию. К тому времени появилось уже несколько утилит в стиле godep
. Мэтт Фарина опубликовал пост «Путешествие по морю пакетных менеджеров Go», сравнивая godep
с новичками, особенно glide
.
В апреле 2015 года Дэйв Чейни представил gb, «инструмент сборки на основе проекта… с повторяемой сборкой через вендоринг исходников», опять же без перезаписи путей импорта (другая мотивация для создания gb заключалась в том, чтобы избежать требования хранить код в определённых каталогах в GOPATH, что не всегда удобно).
Той весной Джейсон Буберель изучил ситуацию с системами управления пакетами Go, в том числе многочисленное дублирование усилий и напрасную работу над похожими утилитами. Его опрос дал понять разработчикам, что поддержку вендоринга без перезаписи путей импорта обязательно надо добавить в команду go
. В то же время, Даниэль Теофанес начал подготовку спецификации для формата файла, который описывает точное происхождение и версию кода в каталоге вендора. В июне 2015 года мы приняли предложение Кита в качестве эксперимента по вендорингу в Go 1.5, который включили по умолчанию в Go 1.6. Мы призвали авторов всех инструментов для вендоринга поработать с Даниэлем, чтобы принять единый формат файла метаданных.
Внедрение концепции вендоринга в Go позволило инструментам вроде vet
более грамотно анализировать программы, и сегодня её используют уже с десяток или два пакетных менеджеров или инструментов вендоринга. С другой стороны, поскольку у всех различные форматы метаданных, они не взаимодействуют и не могут легко обмениваться информацией о зависимостях.
Более фундаментально, вендоринг — неполное решение проблемы управления версиями. Он обеспечивает только воспроизводимость сборки, но не помогает разобраться в версиях пакета и решить, какую из них использовать. Пакетные менеджеры вроде glide
и dep
неявно добавляют в Go концепцию управления версиями, определённым образом настраивая каталог вендора. В результате многие инструменты в экосистеме Go не могут быть получить корректную информацию о версиях. Понятно, что Go нуждается в прямой поддержке версий пакетов.
Официальный эксперимент по управлению пакетами
На GopherCon 2016 в Hack Day (теперь Community Day) собралась группа активистов Go для широкого обсуждения проблемы управления пакетами. Одним из результатов стало формирование комитета и консультативной группы для ведения комплекса работ с целью создания нового инструмента управления пакетами. Идея заключалась в том, чтобы унифицированный инструмент заменил существующие, хотя он всё равно будет реализован за рамками прямого инструментария Go, с использованием каталогов вендоров. В комитет вошли Эндрю Джерранд, Эд Мюллер, Джесси Фразель и Сэм Бойер под руководством Питера Бургона. Они подготовили черновик спецификаций, а затем Сэм с помощниками реализовали dep. Для понимания общей ситуации см. статью Сэма в феврале 2016 года «Итак, вы хотите написать пакетный менеджер», его пост в декабре 2016 года «Сага об управлении зависимостями в Go» и выступление в июле 2017 года на GopherCon «Новая эра управления пакетами в Go».
Dep
выполняет много задач: это важное улучшение по сравнению с текущими практиками. Это и важный шаг к будущему решению, и одновременно эксперимент — мы называем его «официальным экспериментом» — который помогает нам лучше узнать потребности разработчиков. Но dep
не является прямым прототипом возможной интеграции команд go
в управление версиями пакетов. Это мощный, гибкий, почти универсальный способ исследовать пространство проектных решений. Он похож на makefiles, с которыми мы боролись в самом начале. Но как только мы лучше поймём пространство проектных решений и сможем сузить его до нескольких ключевых функций, которые должны поддерживаться, это поможет экосистеме Go удалить другие функции, уменьшить выразительность, принять обязательные конвенции, которые делают базы кода Go более единообразными и лёгкими в понимании.
Эта статья является началом слудующего шага после dep
: первый прототип окончательной интеграции с командой go
, пакетный эквивалент goinstall
. Прототип — это отдельная команда, которую мы называем vgo
: замена go
с поддержкой управления версиями пакетов. Это новый эксперимент, и мы посмотрим, что из него выйдет. Также как и во время анонса goinstall
, некоторые проекты и код уже сейчас совместимы с vgo
, а другие нуждаются в изменениях. Мы уберём некоторый контроль и выразительность, также как в своё время убрали makefiles, в целях упрощения системы и устранения сложности для пользователей. Самое главное, мы ищем первопроходцев, которые помогут экспериментировать с vgo
, чтобы получить как можно больше отзывов.
Начало эксперимента с vgo
не означает прекращения поддержки dep
: он останется доступным до тех пор, пока мы не достигнем полной и общедоступной интеграции с go
. Мы также постараемся сделать окончательный переход от dep
к интеграции с go
как можно более плавным, в какой бы форме не осуществлялась эта интеграция. Проекты, которые ещё не преобразованы в dep
, по-прежнему могут извлечь реальную выгоду из этого преобразования (обратите внимание, что godep
и glide
прекратили активное развитие и поощряют миграцию на dep). Возможно, какие-то проекты пожелают перейти сразу на vgo
, если это отвечает их потребностям.
Предложение по добавлению управления версиями в команду go
состоит из четырёх шагов. Во-первых, принять правило совместимости импорта, на которое указывают FAQ и gopkg.in: более новые версии пакета с заданным путём импорта должны быть обратно совместимы со старыми версиями. Во-вторых, принять простой новый алгоритм, известный как выбор минимальной версии для определения, какие версии пакета используются в данной сборке. В-третьих, ввести понятие модуля Go: группы пакетов, версионированных как единое целое и объявляющих минимальные требования, которые должны быть удовлетворены их зависимостями. В-четвёртых, определить, как встроить всё это в существующую команду go
, чтобы основные рабочие процессы существенно не изменились с сегодняшнего дня. В остальной части статьи мы рассматриваем каждый из этих шагов. Они более подробно рассматриваются в других статьях блога.
Правило совместимости импорта
Главная проблема систем управления пакетами — попытки решить несовместимости. Например, большинство систем позволяют пакету B объявить, что ему нужен пакет D версии 6 или более поздней, а затем позволяют пакету C объявить, что он требует D версии 2, 3 или 4, но не 5-й или более поздней версии. Таким образом, если в своём пакете вы хотите использовать B и C, то вам не повезло: невозможно выбрать ни одной версии D, которая удовлетворяет обоим условиям, и вы ничего не можете сделать.
Вместо системы, которая неизбежно блокирует сборку больших программ, наше предложение вводит правило совместимости импорта для авторов пакетов:
Если у старого и нового пакетов одинаковый путь импорта, новый пакет должен быть обратно совместим со старым пакетом.
Правило повторяет FAQ, упомянутый ранее. Тот текст завершался словами: «В случае кардинального изменения создайте новый пакет с новым путём импорта». Сегодня для такого кардинального изменения разработчики рассчитывают на семантическое управление версиями, поэтому мы интегрируем его в наше предложение. В частности, номер второй и последующих основных версий можно непосредственно включать в путь:
import "github.com/go-yaml/yaml/v2"
В семантическом управлении версиями версия 2.0.0 означает кардинальное изменение, поэтому создаётся новый пакет с новым путём импорта. Поскольку у каждой основной версии другой путь импорта, то конкретный исполняемый файл Go может содержать одну из основных версий. Это ожидаемо и желательно. Такая система поддерживает сборку программ и позволяет частям очень большой программы независимо друг от друга обновиться с v1 на v2.
Соблюдение авторами правила совместимости импорта устраняет попытки решить несовместимости, экспоненциально упрощая общую систему и уменьшая фрагментированность экосистемы пакетов. Конечно, на практике, несмотря на все старания авторов, обновления в рамках одной и той же основной версии иногда ломают пакеты пользователей. Поэтому не следует слишком часто обновляться. Это подводит нас к следующему шагу.
Выбор минимальной версии
Сегодня почти все пакетные менеджеры, включая dep
и cargo
, используют в сборке самую последнюю разрешённую версию пакетов. Я считаю, что такое поведение по умолчанию неправильно по двум причинам. Во-первых, номер «последней разрешённой версии» может измениться из-за внешних событий, а именно из-за публикации новых версий. Возможно, сегодня вечером кто-то представит новую версию некоторой зависимости, а завтра та же последовательность команд, которую вы выполнили сегодня, даст другой результат. Во-вторых, чтобы переопределить это значение по умолчанию, разработчики тратят своё время, указывая пакетному менеджеру «нет, не нужно использовать X», а затем пакетный менеджер тратит время на поиск способа не использовать X.
В нашем предложении используется другой подход, который я называю выбор минимальной версии. По умолчанию используется самая старая разрешённая версия каждого пакета. Это решение не изменится завтра, потому что невозможно опубликовать более старую версию. Ещё лучше, что для пакетного менеджера тривиально определить, какую версию использовать. Я называю это выбором минимальной версии, потому что выбранные номера версии минимальны, а ещё потому что система в целом, возможно, тоже минимальна, избегая почти всей сложности существующих систем.
Выбор минимальной версии позволяет модулям задавать только минимальные требования к зависимостям. Это чётко определённые, уникальные ответы как для обновлений, так и для операций понижения версии, и эти операции действительно эффективны. Такой принцип позволяет автору всего модуля указать версии зависимостей, которые он хочет исключить, или указать замену конкретной зависимости на её форк, который размещается или в локальном хранилище, или опубликован как отдельный модуль. Эти исключения и замены не применяются, когда модуль собирается как зависимость какого-то другого модуля. Это даёт пользователям полный контроль над тем, как собираются их собственные программы, но не над чужими.
Выбор минимальной версии обеспечивает воспроизводимые сборки по умолчанию без файла блокировки.
Совместимость импорта — ключ к простоте выбора минимальной версии. Пользователи больше не могут сказать «нет, это слишком новая версия», они могут только сказать «нет, она слишком старая». В этом случае решение понятно: используйте (минимально) более новую версию. И более новые версии по соглашению являются приемлемыми заменами для более старых.
Определение модулей Go
Модуль Go представляет собой набор пакетов с общим префиксом пути импорта, известным как путь модуля. Модуль является единицей управления версиями, а версии записываются в виде семантических строк. При разработке с помощью Git разработчики определяют новую семантическую версию модуля, добавляя тег в репозиторий Git модуля. Хотя сильно рекомендуется указывать именно семантические версии, поддерживаются и ссылки на конкретные коммиты.
В новом файле go.mod
модуль определяет минимальные требования к версии других модулей, от которых он зависит. Например, вот простой файл go.mod
:
// My hello, world.
module "rsc.io/hello"
require (
"golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
"rsc.io/quote" v1.5.2
)
Этот файл определяет модуль, который идентифицируется по пути rsc.io/hello
, а сам зависит от двух других модулей: golang.org/x/text
и rsc.io/quote
. Сборка модуля сама по себе всегда будет использовать определённые версии необходимых зависимостей, перечисленные в файле go.mod
. Как часть более крупной сборки, он может использовать более новую версию только если какая-то другая часть сборки этого потребует.
Авторы помечают свои релизы семантическими версиями, а vgo
рекомендует использовать помеченные версии, а не произвольные коммиты. У модуля rsc.io/quote
, который поставляется с github.com/rsc/quote
, есть помеченные версии, в том числе 1.5.2. Однако у модуля golang.org/x/text
помеченных версий ещё нет. Чтобы присвоить название непомеченным коммитам, псевдоверсия v0.0.0-yyyymmddhhmmss-commit определяет конкретный коммит в заданную дату. В семантическом управлении версиями эта строка соответствует пререлизу v0.0.0 с идентификатором yyyymmddhhmmss-commit. Семантические правила старшинства версий распознают такие пререлизы как более ранние, чем версия v0.0.0, и выполняют сравнение строки. Порядок указания даты в псевдоверсии гарантирует, что сравнение строк соответствует сравнению дат.
В дополнение к этим требованиям, файлы go.mod
могут указывать исключения и замены, упомянутые в предыдущем разделе, но опять же они применяются только при сборке изолированного модуля, а не при сборке в рамках более крупной программы. Всё это демонстрируется в примерах.
Goinstall
и старый go get
для загрузки кода вызывают инструменты управления версиями, такие как git
и hg
, что приводит ко многим проблемам, среди которых фрагментация. Например, пользователи без bzr
не могут загрузить код из репозиториев Bazaar. В отличие от этой системы, модули Go всегда выдаются по HTTP в виде zip-архивов. Раньше в go get
были особые команды для популярных хостингов кода. Сейчас у vgo
особые процедуры API для получения архивов с этих сайтов.
Единообразное представление модулей в виде zip-архивов позволяет тривиально реализовать протокол и прокси-сервер для загрузки модулей. У компаний и отдельных пользователей есть разные причины для запуска таких прокси-серверов, в том числе безопасность и желание работать с кэшированными копиями в случае удаления оригиналов. При наличии прокси для обеспечения доступности и go.mod
для определения, какой код использовать, больше не нужны каталоги вендоров.
Команда go
Для работы с модулями команду go
нужно обновить. Одним из существенных изменений является то, что обычные команды сборки, такие как go build
, go install
, go run
и go test
, начнут разрешать по требованию новые зависимости. Чтобы использовать golang.org/x/text
в совершенно новом модуле достаточно добавить импорт в исходный код Go и выполнить сборку.
Но самое значительное изменение — прощание с GOPATH как местом написания кода. Поскольку файл go.mod
включает в себя полный путь к модулю, а также определяет версию каждой используемой зависимости, каталог с файлом go.mod
отмечает корень дерева каталогов, который служит автономным рабочим пространством, отдельно от любых других таких каталогов. Теперь вы просто делаете git clone
, cd
, и начинаете писать. Где угодно. Никакого GOPATH.
Я опубликовал также «Тур по управлению версиями в Go» с демонстрацией, как использовать vgo
. В той статье рассказывается, как скачать и прямо сегодня начать использовать vgo
. Остальная информация в других статьях. Буду рад комментариям.
Пожалуйста, попробуйте vgo. Начните размечать семантическими тегами версии в репозиториях. Создавайте файлы go.mod
. Обратите внимание, что если в репозитории пустой файл go.mod
, но есть dep
, glide
, glock
, godep
, godeps
, govend
, govendor
или конфигурационный файл gvt
, то vgo
использует их для заполнения файла go.mod
.
Я рад, что Go делает этот давно назревший шаг по поддержке версий. Некоторые из наиболее распространённых проблем, с которыми сталкиваются разработчики Go, — отсутствие воспроизводимых сборок, полное игнорирование тегов релиза со стороны go get
, неспособность GOPATH распознать разные версии пакета и невозможность работать в каталогах за пределами GOPATH. Предлагаемый здесь дизайн устраняет все эти проблемы и многое другое.
Но я наверняка ошибаюсь в каких-то деталях. Надеюсь, читатели помогут улучшить его, испытав прототип vgo
и участвуя в продуктивных дискуссиях. Я бы хотел, чтобы Go 1.11 поставлялся с предварительной поддержкой модулей Go, как своего рода демо, а затем Go 1.12 вышел с официальной поддержкой. В более поздних версиях мы удалим поддержку старого, неверсионного go get
. Но это агрессивный план, и если для правильной функциональности потребуется ждать более поздних релизов, так тому и быть.
Меня очень волнует переход от старого go get
и бесчисленных инструментов вендоринга к новой модульной системе. Этот процесс так же важен для меня, как и правильная функциональность. Если успешный переход означает ожидание более поздних выпусков, опять же, так тому и быть.