Хабрахабр

[Перевод] Go += управление версиями пакетов

Статья написана в феврале 2018 года

В Go необходимо добавить версионирование пакетов.

Команда go должна точно говорить, какие версии каких пакетов находятся в конкретной сборке. Точнее, нужно добавить концепцию версионирования в рабочий словарь разработчиков Go и в инструменты, чтобы все употребляли одинаковые номера версий при упоминании, какую именно программу собрать, запустить или проанализировать.

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

Даже если вышли новые версии зависимостей, go не станет их использовать без специальной команды.
Хотя нужно добавить управление версиями, не следует отказываться от главных преимуществ команды go: это простота, скорость и понятность. Версионирование также гарантирует, что завтра программа соберётся точно так же, как сегодня. Если сделать правильную модель, то программисты по-прежнему не будут обращать внимания на номера версий, просто всё станет лучше работать и станет понятнее. Сегодня многие программисты не обращают внимания на версии, и всё работает нормально. Выпуск новых версий очень простой. Существующие рабочие процессы практически не изменятся. В общем, управление версиями должно уйти на второй план и не отнимать внимание разработчика.

В этой статье мы предлагаем, как это сделать, а также демонстрируем прототип, который вы можете попробовать уже сейчас, и который, надеюсь, станет основой для возможной интеграции go. Короче говоря, нужно добавить управление версиями пакета, но не сломать go get. На основе этого обсуждения я внесу коррективы как в своё предложение, так и в прототип, а затем представлю официальное предложение для добавления опциональной функции в Go 1. Надеюсь, статья станет началом продуктивной дискуссии о том, что работает, а что нет. 11.

Тем не менее, это предложение все ещё на ранней стадии. Это предложение сохраняет все преимущества go get, но добавляет воспроизводимые сборки, поддерживает семантическое управление версиями, устраняет вендоринг, убирает GOPATH в пользу рабочего процесса на основе проекта и обеспечивает плавный уход от dep и его предшественников. Если детали не верны, мы их исправим, прежде чем работа попадёт в основной дистрибутив Go.

Прежде чем изучить предложение, давайте посмотрим на текущее положение и как мы в нём оказались. Может, этот раздел слегка великоват, но история несёт важные уроки и помогает понять, почему мы хотим что-то изменить. Если это вам не интересно, можете сразу перейти к предложению или прочитать сопроводительную статью в блоге с примером.

Makefile, goinstall и go get

В ноябре 2009 года с первоначальной версией Go вышел компилятор, компоновщик и несколько библиотек. Чтобы скомпилировать и связать программы, требовалось запустить 6g и 6l, и мы включили в комплект примеры файлов makefile. Минимальная оболочка gobuild могла собрать один пакет и написать соответствующий makefile (в большинстве случаев). Не было никакого установленного способа поделиться кодом с другими. Мы знали, что этого мало — но выпустили то, что было, планируя разработать остальное совместно с сообществом.

Goinstall ввёл конвенции по путям импорта, которые сейчас считаются общепринятыми. В феврале 2010 года мы предложили goinstall, простую команду для загрузки пакетов из репозиториев систем управления версиями, таких как Bitbucket и GitHub. Но разработчики быстро перешли к единому соглашению, которое мы знаем сегодня, и набор опубликованных пакетов Go вырос в целостную экосистему. Но в то время ни один код не следовал этим соглашениям, goinstall сначала работал только с пакетами, которые не импортировали ничего, кроме стандартной библиотеки.

Хотя иногда бывает неудобно, что авторы пакетов не могут генерировать код во время каждой сборки, это упрощение невероятно важно для пользователей пакета: им не нужно беспокоиться об установке того же набора инструментов, который использовал автор. Goinstall также устранил файлы Makefile, а с ними и сложность пользовательских вариантов сборки. Makefile — это обязательный пошаговый рецепт компиляции пакета; а применение к тому же пакету другого инструмента вроде go vet или автозавершения может оказаться довольно сложным. Такое упрощение также имеет решающее значение для работы инструментов. Хотя в то время некоторые возражали, что их лишают гибкости, но оглядываясь назад, становится ясно, что отказ от Makefile стал правильным шагом: выгоды намного перевешивают неудобства. Даже правильное получение зависимостей, чтобы собирать заново пакеты при необходимости и только при необходимости, намного сложнее с произвольными файлами Makefile.

В декабре 2011 года в рамках подготовки Go 1 мы представили команду go, которая заменила goinstall на go get.

Он также изолировал детали системы сборки внутри команды go, так что стала возможной значительная автоматизация с помощью инструментария. В целом, go get представил значительные изменения: он позволил разработчикам Go обмениваться исходным кодом и использовать работы друг друга. В самых первых обсуждениях goinstall стало понятно: нужно что-то делать с управлением версиями. Но go get не хватает концепции управления версиями. По крайней мере, мы в команде Go этого ясно не понимали. К сожалению, не было понятно, что именно делать. Такая «работа вслепую» привела по крайней мере к двум существенным недостаткам. Когда go get запрашивает пакет, то всегда получает последнюю копию, делегируя операции загрузки и обновления системе управления версиями, такой как Git или Mercurial.

Управление версиями и стабильность API

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

2 добавлена запись FAQ с таким советом относительно версионирования (текст не изменился к версии Go 1. В ноябре 2013 года в версии 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 добавили ещё и такой совет:

Сохраните копию c новым путём импорта, который идентифицирует её как локальную копию. Если вы используете внешний пакет и опасаетесь, что он может неожиданно измениться, самое простое решение — скопировать его в локальный репозиторий (такой подход используется в Google). Один из инструментов для этой процедуры — goven Кита Рэрика. Например, можно скопировать original.com/pkg в you.com/external/original.com/pkg.

Кит Рэрик начал этот проект в марте 2012 года. Утилита goven копирует зависимость в локальный репозиторий и обновляет все пути импорта, чтобы отразить новое местоположение. Такие изменения исходного кода необходимы, но неприятны. Они затрудняют сравнение и включение новых копий, а также требуют обновления другого скопированного кода с использованием этой зависимости.

Основным достижением godep стало то, что мы теперь называем вендорингом, то есть копирование зависимостей в проект без изменения исходных файлов, без прямой поддержки инструментов, путём определённой настройки GOPATH. В сентябре 2013 года Кит представил godep, «новый инструмент для фиксации зависимостей пакетов».

К тому времени появилось уже несколько утилит в стиле godep. В октябре 2014 года Кит предложил добавить в инструменты Go поддержку «внешних пакетов», чтобы инструменты лучше понимали проекты, использующие эту конвенцию. Мэтт Фарина опубликовал пост «Путешествие по морю пакетных менеджеров Go», сравнивая godep с новичками, особенно glide.

В апреле 2015 года Дэйв Чейни представил gb, «инструмент сборки на основе проекта… с повторяемой сборкой через вендоринг исходников», опять же без перезаписи путей импорта (другая мотивация для создания gb заключалась в том, чтобы избежать требования хранить код в определённых каталогах в GOPATH, что не всегда удобно).

Его опрос дал понять разработчикам, что поддержку вендоринга без перезаписи путей импорта обязательно надо добавить в команду go. Той весной Джейсон Буберель изучил ситуацию с системами управления пакетами Go, в том числе многочисленное дублирование усилий и напрасную работу над похожими утилитами. В июне 2015 года мы приняли предложение Кита в качестве эксперимента по вендорингу в Go 1. В то же время, Даниэль Теофанес начал подготовку спецификации для формата файла, который описывает точное происхождение и версию кода в каталоге вендора. 6. 5, который включили по умолчанию в Go 1. Мы призвали авторов всех инструментов для вендоринга поработать с Даниэлем, чтобы принять единый формат файла метаданных.

С другой стороны, поскольку у всех различные форматы метаданных, они не взаимодействуют и не могут легко обмениваться информацией о зависимостях. Внедрение концепции вендоринга в Go позволило инструментам вроде vet более грамотно анализировать программы, и сегодня её используют уже с десяток или два пакетных менеджеров или инструментов вендоринга.

Он обеспечивает только воспроизводимость сборки, но не помогает разобраться в версиях пакета и решить, какую из них использовать. Более фундаментально, вендоринг — неполное решение проблемы управления версиями. В результате многие инструменты в экосистеме Go не могут быть получить корректную информацию о версиях. Пакетные менеджеры вроде glide и dep неявно добавляют в Go концепцию управления версиями, определённым образом настраивая каталог вендора. Понятно, что Go нуждается в прямой поддержке версий пакетов.

Официальный эксперимент по управлению пакетами

На GopherCon 2016 в Hack Day (теперь Community Day) собралась группа активистов Go для широкого обсуждения проблемы управления пакетами. Одним из результатов стало формирование комитета и консультативной группы для ведения комплекса работ с целью создания нового инструмента управления пакетами. Идея заключалась в том, чтобы унифицированный инструмент заменил существующие, хотя он всё равно будет реализован за рамками прямого инструментария Go, с использованием каталогов вендоров. В комитет вошли Эндрю Джерранд, Эд Мюллер, Джесси Фразель и Сэм Бойер под руководством Питера Бургона. Они подготовили черновик спецификаций, а затем Сэм с помощниками реализовали dep. Для понимания общей ситуации см. статью Сэма в феврале 2016 года «Итак, вы хотите написать пакетный менеджер», его пост в декабре 2016 года «Сага об управлении зависимостями в Go» и выступление в июле 2017 года на GopherCon «Новая эра управления пакетами в Go».

Это и важный шаг к будущему решению, и одновременно эксперимент — мы называем его «официальным экспериментом» — который помогает нам лучше узнать потребности разработчиков. Dep выполняет много задач: это важное улучшение по сравнению с текущими практиками. Это мощный, гибкий, почти универсальный способ исследовать пространство проектных решений. Но dep не является прямым прототипом возможной интеграции команд go в управление версиями пакетов. Но как только мы лучше поймём пространство проектных решений и сможем сузить его до нескольких ключевых функций, которые должны поддерживаться, это поможет экосистеме Go удалить другие функции, уменьшить выразительность, принять обязательные конвенции, которые делают базы кода Go более единообразными и лёгкими в понимании. Он похож на makefiles, с которыми мы боролись в самом начале.

Прототип — это отдельная команда, которую мы называем vgo: замена go с поддержкой управления версиями пакетов. Эта статья является началом слудующего шага после dep: первый прототип окончательной интеграции с командой go, пакетный эквивалент goinstall. Также как и во время анонса goinstall, некоторые проекты и код уже сейчас совместимы с vgo, а другие нуждаются в изменениях. Это новый эксперимент, и мы посмотрим, что из него выйдет. Самое главное, мы ищем первопроходцев, которые помогут экспериментировать с vgo, чтобы получить как можно больше отзывов. Мы уберём некоторый контроль и выразительность, также как в своё время убрали makefiles, в целях упрощения системы и устранения сложности для пользователей.

Мы также постараемся сделать окончательный переход от dep к интеграции с go как можно более плавным, в какой бы форме не осуществлялась эта интеграция. Начало эксперимента с vgo не означает прекращения поддержки dep: он останется доступным до тех пор, пока мы не достигнем полной и общедоступной интеграции с go. Возможно, какие-то проекты пожелают перейти сразу на vgo, если это отвечает их потребностям. Проекты, которые ещё не преобразованы в dep, по-прежнему могут извлечь реальную выгоду из этого преобразования (обратите внимание, что godep и glide прекратили активное развитие и поощряют миграцию на dep).

Предложение по добавлению управления версиями в команду 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. Как часть более крупной сборки, он может использовать более новую версию только если какая-то другая часть сборки этого потребует.

У модуля rsc.io/quote, который поставляется с github.com/rsc/quote, есть помеченные версии, в том числе 1. Авторы помечают свои релизы семантическими версиями, а vgo рекомендует использовать помеченные версии, а не произвольные коммиты. 2. 5. Чтобы присвоить название непомеченным коммитам, псевдоверсия v0. Однако у модуля golang.org/x/text помеченных версий ещё нет. 0-yyyymmddhhmmss-commit определяет конкретный коммит в заданную дату. 0. 0. В семантическом управлении версиями эта строка соответствует пререлизу v0. Семантические правила старшинства версий распознают такие пререлизы как более ранние, чем версия v0. 0 с идентификатором yyyymmddhhmmss-commit. 0, и выполняют сравнение строки. 0. Порядок указания даты в псевдоверсии гарантирует, что сравнение строк соответствует сравнению дат.

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

Например, пользователи без bzr не могут загрузить код из репозиториев Bazaar. Goinstall и старый go get для загрузки кода вызывают инструменты управления версиями, такие как git и hg, что приводит ко многим проблемам, среди которых фрагментация. Раньше в go get были особые команды для популярных хостингов кода. В отличие от этой системы, модули Go всегда выдаются по HTTP в виде zip-архивов. Сейчас у vgo особые процедуры API для получения архивов с этих сайтов.

У компаний и отдельных пользователей есть разные причины для запуска таких прокси-серверов, в том числе безопасность и желание работать с кэшированными копиями в случае удаления оригиналов. Единообразное представление модулей в виде zip-архивов позволяет тривиально реализовать протокол и прокси-сервер для загрузки модулей. При наличии прокси для обеспечения доступности и go.mod для определения, какой код использовать, больше не нужны каталоги вендоров.

Команда go

Для работы с модулями команду go нужно обновить. Одним из существенных изменений является то, что обычные команды сборки, такие как go build, go install, go run и go test, начнут разрешать по требованию новые зависимости. Чтобы использовать golang.org/x/text в совершенно новом модуле достаточно добавить импорт в исходный код Go и выполнить сборку.

Поскольку файл go.mod включает в себя полный путь к модулю, а также определяет версию каждой используемой зависимости, каталог с файлом go.mod отмечает корень дерева каталогов, который служит автономным рабочим пространством, отдельно от любых других таких каталогов. Но самое значительное изменение — прощание с GOPATH как местом написания кода. Где угодно. Теперь вы просто делаете git clone, cd, и начинаете писать. Никакого GOPATH.

Я опубликовал также «Тур по управлению версиями в Go» с демонстрацией, как использовать vgo. В той статье рассказывается, как скачать и прямо сегодня начать использовать vgo. Остальная информация в других статьях. Буду рад комментариям.

Начните размечать семантическими тегами версии в репозиториях. Пожалуйста, попробуйте vgo. Обратите внимание, что если в репозитории пустой файл go.mod, но есть dep, glide, glock, godep, godeps, govend, govendor или конфигурационный файл gvt, то vgo использует их для заполнения файла go.mod. Создавайте файлы go.mod.

Некоторые из наиболее распространённых проблем, с которыми сталкиваются разработчики Go, — отсутствие воспроизводимых сборок, полное игнорирование тегов релиза со стороны go get, неспособность GOPATH распознать разные версии пакета и невозможность работать в каталогах за пределами GOPATH. Я рад, что Go делает этот давно назревший шаг по поддержке версий. Предлагаемый здесь дизайн устраняет все эти проблемы и многое другое.

Надеюсь, читатели помогут улучшить его, испытав прототип vgo и участвуя в продуктивных дискуссиях. Но я наверняка ошибаюсь в каких-то деталях. 11 поставлялся с предварительной поддержкой модулей Go, как своего рода демо, а затем Go 1. Я бы хотел, чтобы Go 1. В более поздних версиях мы удалим поддержку старого, неверсионного go get. 12 вышел с официальной поддержкой. Но это агрессивный план, и если для правильной функциональности потребуется ждать более поздних релизов, так тому и быть.

Этот процесс так же важен для меня, как и правильная функциональность. Меня очень волнует переход от старого go get и бесчисленных инструментов вендоринга к новой модульной системе. Если успешный переход означает ожидание более поздних выпусков, опять же, так тому и быть.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»