The Go Blog
Предложение по управлению версиями пакетов в Go
Введение
Восемь лет назад команда Go представила goinstall
(что привело к go get)
и с ней — децентрализованные, похожие на URL пути импорта,
с которыми знакомы сегодня разработчики Go.
После выпуска goinstall первым вопросом, который задавали люди,
было: как добавить информацию о версии.
Мы признавали, что не знали ответа.
На протяжении длительного времени мы полагали, что проблема управления версиями пакетов
лучше всего решится с помощью дополнительного инструмента,
и мы поощряли людей создавать такие инструменты.
Сообщество Go создало множество инструментов с разными подходами.
Каждый из них помог нам лучше понять проблему,
но к середине 2016 года стало очевидно, что решений стало слишком много.
Нам требовался один официальный инструмент.
После начала обсуждения в GopherCon в июле 2016 года и его продолжения до осени,
мы все считали, что правильным решением будет следовать подходу к управлению версиями пакетов,
примером которого является Cargo в Rust, с тегированными семантическими версиями,
манифестом, файлом блокировки и
SAT-решателем для принятия решения о том, какие версии использовать.
Сэм Бойер возглавил команду, которая создала Dep, следуя этой общей схеме,
и которую мы намеревались использовать как модель интеграции в команду go.
Но по мере того как мы узнали больше о последствиях подхода Cargo/Dep,
мне стало ясно, что Go будет лучше, если изменить
некоторые детали, особенно касающиеся обратной совместимости.
Влияние совместимости
Самым важным новым элементом Go 1 не была языковая особенность. Это была акцент на обратной совместимости в Go 1. До этого мы выпускали стабильные снапшоты примерно раз в месяц, каждый из которых содержал существенные несовместимые изменения. Мы заметили значительный рост интереса и внедрения сразу после выхода Go 1. Мы полагаем, что обещание совместимости сделало разработчиков гораздо более уверенными в использовании Go для промышленного применения, и это ключевая причина популярности Go сегодня. С 2013 года FAQ Go поощряет разработчиков пакетов предоставлять своим пользователям похожие ожидания совместимости. Мы называем это правилом совместимости импорта: «Если старый пакет и новый пакет имеют одинаковый путь импорта, новый пакет должен быть совместим с предыдущей версией».
Независимо от этого, семантическое версионирование стало де-факто стандартом описания версий программного обеспечения в многих языковых сообществах, включая сообщество Go. При использовании семантического версионирования, более поздние версии предполагаются совместимыми с более ранними, но только в пределах одной основной версии: v1.2.3 должна быть совместима с v1.2.1 и v1.1.5, но v2.3.4 не обязана быть совместима ни с одной из них.
Если мы примем семантическое версионирование для пакетов Go,
как ожидают большинство разработчиков на Go,
то правило совместимости импортов требует,
что различные мажорные версии должны использовать разные пути импорта.
Это наблюдение привело нас к семантическому импортному версионированию,
при котором версии, начинающиеся с v2.0.0, включают номер мажорной версии
в путь импорта: my/thing/v2/sub/pkg.
Год назад я сильно верил, что включать номера версий в пути импорта — в основном вопрос вкуса, и я был скептически настроен к тому, чтобы это было особенно элегантно. Но оказалось, что этот выбор — дело не вкуса, а логики: совместимость импортов и семантическое версионирование вместе требуют семантического импортного версионирования. Когда я это осознал, логическая необходимость удивила меня.
Меня также удивило, что существует второй, независимый логический путь к семантическому импортному версионированию: постепенное исправление кода или частичное обновление кода. В большом приложении не réalно ожидать, что все пакеты в программе обновятся с v1 до v2 определённой зависимости одновременно. Вместо этого должно быть возможно, чтобы некоторые части программы продолжали использовать v1, а другие части обновились до v2. Но тогда сборка программы и её финальный бинарный файл должны включать и v1, и v2 зависимости. Если бы они использовали один и тот же путь импорта, это привело бы к путанице, что нарушает так называемое правило уникальности импортов: разные пакеты должны иметь разные пути импорта. Единственный способ обеспечить частичные обновления кода, уникальность импортов и семантическое версионирование — это также принять семантическое импортное версионирование.
Конечно, возможно построить системы, использующие семантическое версионирование без семантического импортного версионирования, но только за счёт отказа от частичных обновлений кода или уникальности импортов. Cargo позволяет частичные обновления кода, отказываясь от уникальности импортов: данный путь импорта может иметь разное значение в разных частях большой сборки. Dep обеспечивает уникальность импортов, отказываясь от частичных обновлений кода: все пакеты, участвующие в большой сборке, должны найти единственную согласованную версию заданной зависимости, что повышает риск того, что большие программы не смогут быть собраны. Cargo прав, настаивая на частичных обновлениях кода, которые критически важны для разработки масштабного программного обеспечения. Dep также прав, настаивая на уникальности импортов. Сложные случаи использования текущей поддержки vendoring в Go могут нарушать уникальность импортов. Когда это происходит, возникающие проблемы довольно сложны для понимания как разработчиками, так и инструментами. Выбор между частичными обновлениями кода и уникальностью импортов требует предсказания, что будет дороже потерять. Семантическое импортное версионирование позволяет избежать такого выбора и сохранить оба подхода.
Меня также удивило, насколько семантическая совместимость импортов упрощает выбор версий, которая является задачей определения, какие версии пакетов использовать для заданной сборки. Ограничения Cargo и Dep делают выбор версий эквивалентным решению задачи выполнимости булевых формул, то есть может быть очень дорогостоящим определить, существует ли вообще допустимая конфигурация версий. И даже если существует несколько допустимых конфигураций, не существует однозначных критериев выбора «лучшей» из них. Использование семантической совместимости импортов позволяет Go использовать тривиальный алгоритм линейного времени, чтобы найти единственную лучшую конфигурацию, которая всегда существует. Этот алгоритм, который я называю минимальный выбор версий, в свою очередь устраняет необходимость в отдельных файлах lock и manifest. Он заменяет их одним коротким конфигурационным файлом, редактируемым напрямую как разработчиками, так и инструментами, при этом сохраняя возможность воспроизводимой сборки.
Наш опыт с использованием Dep демонстрирует влияние совместимости. Следуя примеру Cargo и более ранних систем, мы спроектировали Dep таким образом, чтобы он отказывался от совместимости импортов как часть принятия семантического версионирования. Я не считаю, что мы решили это осознанно; мы просто последовали за другими системами. Первичный опыт использования Dep помог нам лучше понять, насколько велика сложность, возникающая при разрешении несовместимых путей импортов. Возвращение правила совместимости импортов путем введения семантического версионирования импортов устраняет эту сложность, приводя к гораздо более простой системе.
Прогресс, прототип и предложение
Dep был выпущен в январе 2017 года. Его базовая модель — код, помеченный семантическими версиями, а также конфигурационный файл, указывающий требования к зависимостям — была явным шагом вперёд по сравнению с большинством инструментов управления зависимостями в Go, и сама конвергенция к Dep также была явным шагом вперёд. Я искренне поощрял его принятие, особенно с целью помочь разработчикам привыкнуть к мысли о версиях пакетов Go, как для их собственного кода, так и для зависимостей. Хотя Dep ясно двигал нас в правильном направлении, у меня остались подозрения относительно сложности, скрывающейся в деталях. Особенно меня беспокоило отсутствие поддержки постепенных обновлений кода в больших программах. В течение 2017 года я разговаривал с множеством людей, включая Сэма Бойера и остальную часть рабочей группы по управлению пакетами, но никто из нас не смог увидеть никакого простого способа снизить сложность (хотя я находил множество подходов, которые лишь увеличивали её). Подходя к концу года, казалось, что решением могут быть солверы SAT и неудовлетворительные сборки — это было лучшее, что можно было сделать.
В середине ноября, пытаясь ещё раз разобраться, как Dep может поддерживать постепенные обновления кода, я понял, что наш старый подход к совместимости импортов подразумевает семантическое версионирование импортов. Это казалось настоящим прорывом. Я написал первый черновик того, что впоследствии стало моей блог-постом о семантическом версионировании импортов, завершив его предложением, чтобы Dep внедрил этот подход. Я отправил черновик тем людям, с которыми разговаривал, и получил очень сильные реакции: все либо полюбили, либо возненавидели его. Я понял, что мне нужно ещё глубже разобраться с последствиями семантического версионирования импортов, прежде чем распространять идею дальше, и я приступил к этому.
В середине декабря я обнаружил, что совместимость импортов и семантическое версионирование импортов вместе позволяют свести выбор версий к минимальному выбору версий. Я написал базовую реализацию, чтобы убедиться, что понимаю это правильно, потратил некоторое время, чтобы изучить теорию, лежащую в основе его простоты, и написал черновик поста, описывающего это. Однако даже тогда я всё ещё не был уверен, что такой подход будет практичным в реальном инструменте, как Dep. Было ясно, что нужен прототип.
В январе я начал работу над простой обёрткой команды go, реализующей семантическое версионирование импортов и минимальный выбор версий. Простые тесты работали хорошо. Приближаясь к концу месяца, моя простая обёртка могла собирать Dep — реальную программу, использующую множество версионированных пакетов. Обёртка всё ещё не имела интерфейса командной строки — то, что она собирает Dep, было жёстко задано в нескольких строковых константах, но подход был clearly жизнеспособным.
В первые три недели февраля я превратил обёртку в полноценную версионированную команду go, vgo; писал черновики серии блог-постов, представляющих vgo; и обсуждал их с Самом Бойером, рабочей группой по управлению пакетами, и командой Go. А затем в последнюю неделю февраля я наконец поделился vgo и идеями, лежащими в её основе, со всей сообществом Go.
В дополнение к основным идеям совместимости импортов, семантического версионирования импортов и минимального выбора версий, прототип vgo вводит ряд меньших, но значимых изменений, мотивированных восемью годами опыта работы с goinstall и go get: новая концепция Go модуля, который представляет собой коллекцию пакетов, версионированных как единое целое; проверяемые и верифицированные сборки; и осведомлённость о версиях по всему интерфейсу команды go, что позволяет работать вне $GOPATH и устраняет (большинство) каталогов vendor.
Результатом всего этого стало официальное предложение Go, которое я подал на прошлой неделе. Несмотря на то, что оно может выглядеть как полная реализация, это всё ещё только прототип, которым все мы должны будет работать вместе, чтобы завершить. Вы можете загрузить и протестировать прототип vgo с golang.org/x/vgo, а также прочитать Тур по версионированному Go, чтобы получить представление о том, как использовать vgo.
Путь вперёд
Предложение, которое я подал на прошлой неделе, именно такое: начальное предложение. Я знаю, что в нём есть проблемы, которые команда Go и я не можем увидеть, потому что разработчики Go используют Go многими хитрыми способами, о которых мы не знаем. Цель процесса обратной связи по предложениям — это совместная работа всех нас для выявления и решения проблем в текущем предложении, чтобы убедиться, что конечная реализация, появляющаяся в будущем выпуске Go, будет хорошо работать для как можно большего числа разработчиков. Пожалуйста, указывайте проблемы в обсуждаемой задаче предложения. Я буду продолжать обновлять резюме обсуждения и FAQ по мере поступления отзывов.
Для того чтобы этот проект получил успех, всё экосистема Go — и в частности сегодняшние крупные проекты на Go — должны будут принять правило совместимости импортов и семантическое версионирование импортов. Чтобы убедиться, что это произойдёт гладко, мы также проведём сессии обратной связи с пользователями по видеоконференции с проектами, которые имеют вопросы о том, как интегрировать новое предложение по версионированию в свои кодовые базы, или хотят поделиться своим опытом. Если вы заинтересованы в участии в такой сессии, пожалуйста, напишите письмо Стиву Франции по адресу spf@golang.org.
Мы с нетерпением ждём (наконец-то!) предоставления сообществу Go единого, официального ответа на вопрос о том, как интегрировать версионирование пакетов в go get. Благодарим всех, кто помог нам дойти до этого момента, и всех, кто будет помогать в будущем. Мы надеемся, что с вашей помощью мы сможем выпустить что-то, что разработчики Go будут любить.
Следующая статья: Новая марка Go
Предыдущая статья: Результаты опроса Go 2017
Индекс блога