![](https://habrastorage.org/files/dc9/4b6/0ec/dc94b60ec0fa46b3b358c32492bbb8f6.jpeg)
Семантическое версионирование (также известное как SemVer) уже стало популярным подходом к работе с версиями программ и библиотек. Оно не только облегчает работу с последовательными релизами, но позволяет и людям, и автоматике понимать совместимость этих релизов друг с другом и с остальным миром. SemVer можно использовать много где, но больше всего этот подход известен в системах управления зависимостями.
Прежде чем рассказывать о специфичных для Go вещах, давайте посмотрим, что такое семантическое версионирование (примечание переводчика: ради этой части и был затеян перевод).
![](https://habrastorage.org/files/b67/0f8/e31/b670f8e3171c4f5fb7123e930794c6cd.png)
На иллюстрации показаны семантические части строки версии. Чаще всего вы будете видеть только первые три цифры, разделенные точками ".". Полностью же семантическая версия может состоять из следующих частей:
- “Major” номер версии. Увеличивается (и не всегда на 1, вспомните PHP 5 -> 7), когда API библиотеки или приложения меняется обратно несовместимым образом.
- “Minor” номер версии. Увеличивается, когда в API добавляются новые функции без нарушения обратной совместимости (примечание переводчика: вот здесь самое зло. К примеру, npm по умолчанию сохраняет версию с префиксом “^”, что означает “любая версия с такой же major”. Авторы npm резонно полагают, что если при смене minor обратная совместимость не теряется, то можно смело обновлять. Сюрприз: множество разработчиков библиотек просто не знают, что такое семантическое версионирование и ломают обратную совместимость, как получится. Результат – на компьютере автора все работает, а при установке “с чистого листа” на другом компьютере мы получаем новые версии зависимостей с поломанной обратной совместимостью). При увеличении номера “major” принято сбрасывать “minor” в 0, то есть за версией 4.27… последует версия 5.0… а не 5.28
- “Patch” номер версии. Увеличивается при исправлении багов, рефакторинге и прочих изменениях, которые ничего не ломают, но и новых фичей не добавляют. При увеличении “Major” или “Minor” принято сбрасывать “Patch” в 0 (примечание переводчика: очень часто его называют “build number” и просто увеличивают на единицу с каждый билдом continuous integration, никогда не сбрасывая. Это позволяет легко отличать билды друг от друга во время тестирования и анализа сообщений ошибок от пользователей. Минус такого подхода в том, что довольно скоро цифра становится пятизначной. Но многих это не беспокоит).
- Строка “pre-release”: необязательный, разделенный точками список, отделенный от трех номеров версии знаком минус. Например: “1.2.3-beta.2”. Используется вместо тегов, чтобы “помечать” определенные вехи в разработке. Обычно это “alpha”, “beta”, “release candidate” (“rc”) и производные от них.
- Завершает строку семантической версии метаданные системы сборки. Так же, как и список “pre-release” тегов, он разделен точками ., но отделяет от номеров или тегов его не минус, а плюс. Данную информацию принято игнорировать (примечание переводчика: по феншую номер билда должен идти сюда – но мало кто так делает, очень длинные строки получаются).
Несмотря на то, что в спецификации ничего не говорится о префиксе “v”, он часто используется перед строкой семантической версии, например, “v.12.3”. Так же, как и метаданные, его принято игнорировать.
Все это и многое другое можно найти в официальной спецификации: http://semver.org/
Благодаря продуманной спецификации, строки семантических версий можно легко распарсить, сортировать, и, главное, сравнивать друг с другом и с диапазонами «допустимых» версий. Чем, собственно, и занимаются большинство менеджеров зависимостей, таких как npm.
Парсинг семантических версий в Go
Для Go доступно несколько пакетов для работы с семантическими версиями. В этой статье я рассмотрю вот этот: github.com/Masterminds/semver. Он соответствует спецификации, поддерживает необязательный префикс “v”, сортировку, работу с диапазонами и ограничениями. При этом с ограничениями этот пакет работает так же, как большинство решений для других языков программирования, таких как JavaScript, Rust и другие.
Пример ниже парсит строку семантической версии и выводит либо “major” версию, либо сообщение об ошибке:
v, err := semver.NewVersion("1.2.3-beta.1+build345")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(v.Major())
}
Возвращаемое значение является экземпляром semver.Version, который содержит некоторое количество полезных методов. Если переданная строка не является семантической версией, то возвращается ошибка semver.ErrInvalidSemVer.
Но реальная польза этой библиотеки заключается не в возможности парсить строки, а в возможности проводить сложные действия над семантическими действиями.
Сортировка семантических версий
С помощью библиотеки semver вы можете сортировать семантические версии средствами стандартной библиотеки. Например:
raw := []string{"1.2.3", "1.0", "1.0.0-alpha.1" "1.3", "2", "0.4.2",}
vs := make([]*semver.Version, len(raw))
for i, r := range raw {
v, err := semver.NewVersion(r)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}
vs[i] = v
}
sort.Sort(semver.Collection(vs))
В этом примере набор семантических версий преобразуется в экземпляры semver.Version, которые затем складываются в semver.Collection. А у semver.Collection есть все необходимое для использования со стандартной библиотекой sort. Это очень удобно для правильной сортировки pre-release информации и игнорирования мета-тегов.
Диапазоны, Ограничения и Wildcards
Один из самых популярных вопросов относительно версий заключается в проверке, лежит ли версия в указанном диапазоне. Или удовлетворяет ли она каким-то другим ограничениям. Все эти проверки легко сделать с помощью библиотеки:
c, err := semver.NewConstraint(">= 1.2.3, < 2.0.0, != 1.4.5")
if err != nil {
fmt.Println("Error parsing constraint:", err)
return
}
v, err := semver.NewVersion("1.3")
if err != nil {
fmt.Println("Error parsing version:", err)
return
}
a := c.Check(v)
fmt.Println("Version within constraint:", a)
Для тех, кто знаком с диапазонами версий по другим языкам и тулзам, библиотека предлагает хорошо знакомую нотацию:
- ^1.2.3 обозначает совместимость на уровне “major” версии. Это синтаксический сахар для записи “>= 1.2.3, < 2.0.0”. Используется для указания “самой свежей версии зависимости, все еще совместимой по API”.
- ~1.2.3 обозначает совместимость на уровне “patch” версии. Это синтаксический сахар для “>= 1.2.3, < 1.3.0”. Используется для указания нужной версии с последними багфиксами, но без серьезных изменений.
- “1.2.3 — 3.4.5” обозначает точный диапазон версий, включая начальную и конечную. Это синтаксический сахар для “>= 1.2.3, <= 3.4.5”.
- Также библиотека поддерживает “wildcards” с помощью символов “x”, ”X” или “*”. Вы можете указать версию как “2.x”, “1.2.x” или даже как “*”. И все эти нотации можно спокойно комбинировать друг с другом.
Начните использовать семантические версии прямо сейчас
Комментарии (25)
rumkin
14.04.2016 15:24+1Хорошая статья, но я бы не "предлагал", а "настоятельно рекомендовал" использовать semver.
amarao
14.04.2016 17:06+5Простите, а как это соотносится с debian'овской системой нумерации пакетов? И где эпоха в версии? Почему «semver» следует предпочесть существующей модели именования версий, в которой учтено всё на свете — от взбрыков разработчиков по переименованию системы нумерации версии (windows 95 и windows 10), до особенностей системы сборки?
Дборотная версия выглядит примерно так:
2:34.5.6-nmu7+8~20160309142337.19~1.gbp08c0c0
2 — номер эпохи. Означает, что до «34» была какая-то другая версия. Например, «2000», и это «починили».
34.5.6 — что хотите. Версия и версия.
nmu — собиралось не мейнтейнером. Убунтововды там «ubuntu» пишут.
7 — версия сбоки
+ и далее — версия из гита (что именно собиралось). Год, дата, время, потом после тильды — очередная локальная эпоха и gbp (кто собирал) и первые цифры коммита из которого собиралось.
Это я не фантазирую, это индустриальный стандарт.kronoskib
14.04.2016 17:40+2Предполагаю, что чем проще — тем лучше. И потом, это же для мейнстрима — node.js, go, python, ruby, c#, java итд. Для энтерпрайзов с чпуксом наперевес вполне можно использовать и что-то более «тяжеловесное».
amarao
14.04.2016 18:03Это версия питоновской приложухи. Мейнстримовая. Без энтерпрайза.
khim
14.04.2016 18:41+1По моему — это бессмысленный спор о терминах. Есть symver. Фигня вопрос: поддерживается (с некоторыми вариациями) для библиотек (но не дла названий сами приложений!) в GCC и libtool, для пакетов в NPM и в Gentoo, а также в куче разных других мест.
Есть нумерация пакетов в Debian'а. Применяется, вы не поверите, для нумерации пакетов в Debian'е. Причём для библиотек, вы не поверите, Debian использует как раз symver тоже. Правда в извращённом «ручном» виде, но использует (именно поэтому там есть пакеты libc6 и liblzma5, а не просто libc и liblzma).
Что считать мейнстримом — дело ваше. Но если вы собираете библиотеку — то лучше, наверное, использовать систему, предназначенную для работы с библиотеками, а не что-то что используется в совсем других случаях и совсем для другого.
safinaskar
14.04.2016 17:45+2semver — это один из способов нумеровать версии у одного данного конкретного проекта. Система Debian'а — это способ нумеровать версии у пакетов в дистрибутиве, где почти каждый пакет имеет апстрим. Т. е. апстрим выпустил новую версию, стоИт задача правильно дать номер версии для соответствующего пакета Debian. Т. е. задачи разные у этих двух нумераций.
Упомянутый номер эпохи ярко это демонстрирует. Зачем нужен номер эпохи в Debian'е? На случай, если апстрим тупанул и сменил систему нумерации. Допустим, апстим выпускал версии под номерами 10.0, 20.0, 30.0 (ну мало ли), а потом зачем-то решил убрать ноль и выпустил версию номер 4.0. Тогда мейнтейнеры пакета Debian увеличивают на единицу номер эпохи, чтобы не сломалось сравнение номеров версий.
от взбрыков разработчиков по переименованию системы нумерации версии
Ну вот я про это и говорю. Semver не для задачи подготовки пакетов некоего апстримного пакета. Там эти взбрыки учитывать не надо.
Далее, semver на то и называется semver, что в нём учитывается semantic, семантика, смысл. Major, minor и patch выбираются исходя из смысла нового релиза, исходя из его предназначения, исходя из того, насколько сильно он отличается от предыдущих, насколько там важные фичи и так далее, и тому подобное. В Debian'е главный компонент номера версии (34.5.6 в вашем примере) не имеет никакого смысла в рамках всего проекта Debian в целом. Потому что он просто повторяет номер версии апстрима, а тот уже вкладывает в него смысл по своему усмотрению, например, с помощью того же semver.
до особенностей системы сборки?
Эту инфу можно занести в meta semver. Никакого стандарта на устройство этого meta не нужно, meta — это просто for your information, meta игнорируется при сравнении версий
nmu — собиралось не мейнтейнером
Это особенности чисто дебианого процесса разработки. Это не нужно в semver, в системе нумерации для всех и вся.
7 — версия сбоки
Этот номер означает, каким по счёту является данный пакет Debian на основе одного и того же данного upstream пакета. В semver это не нужно, там нет upstream'а и downstream'а.
+ и далее — версия из гита (что именно собиралось). Год, дата, время, потом после тильды — очередная локальная эпоха и gbp (кто собирал) и первые цифры коммита из которого собиралось.
Всё это можно в meta, без стандарта на точное устройство meta, я уже говорил
Kain_Haart
15.04.2016 08:26+334.5.6 — что хотите. Версия и версия.
Вот здесь и предлагается использовать semver
prefrontalCortex
15.04.2016 15:36Спецификация, безусловно, прекрасная, но вот как номер версии, удовлетворяющий ей, сгенерировать автоматически из git — я, например, не представляю :(
Если делать, «как все», добавляя в репозиторий теги с minor.major (например, «v1.0») и вызывать git describe, то patch version будет добавляться через тире, вкупе с хэшем коммита с префиксом g, например, 1.0-42-gdeadbeef.khim
15.04.2016 16:06+2Либо я ничего не понимаю, либо одно из двух. Сгенерировать номер версии автоматически нельзя. Какой смысл в автоматически сгенерированной версии? Кому и о чём она будет говорить? О том конкретно из каких исходников вы собрали ту или иную библиотеку? Ну так это номер билда, а не номер версии.
Номер версии же должен говорить о двух простых вещах: можно ли мне обновляться и стоит ли это делать. symver даёт простые правила: если первая цифра изменилась — обновляться нельзя, так как поломана совместимость. Если последняя цифра изменилась — то обновляться нужно и важно, так как никаких новых вещей в релизе нет, но ошибки и проблемы — поправлены. Если изменилась средняя цифра — то обновляться можно, но нужно всё протестировать (новая функциональность — она, знаете ли, чревата).
Откуда git сможет получить информацию о том — как влияют на код ваши изменения и, соответственно, какую цифирьку увеличивать? Это только вы можете сделать…prefrontalCortex
16.04.2016 18:22Сгенерировать номер версии автоматически
Я не совсем правильно выразился, стоило написать «полуавтоматически». Major/minor в любом случае нужно выбирать самому вручную,
добавляя в репозиторий теги с minor.major (например, «v1.0»)
Zapped
21.04.2016 23:09А по-моему, номер версии должен задаваться сервером сборок и проставляться уже в репозитории тегом. А
git describe
лишь служит тому, чтобы узнать в какую версию вошёл тот или иной коммит
LexxFedoroff
15.04.2016 19:15+1Мы для автоматического генерирования версий используем GitVersion.
Очень удобная штука.
nskazki
18.04.2016 10:27Небольшая ремарка: до первого мажорного релиза ^ работает так же как ~
Например: "^0.2.0" == ">= 0.2.0, < 0.3.0", но "^1.2.0" == ">= 1.2.0, < 2.0.0"
Поэкспериментировать можно здесь: semver.npmjs.com
steff
Разработчики Хрома и Firefox, похоже, не в курсе :)
rumkin
У них гонка версий, а не версионирование. Скоро можно будет топ делать за сколько до сотни доберутся.
Shultc
Когда начнут сразу по 10 за раз прибавлять, так всё ещё быстрее пойдёт!
berezuev
хм… Зашел посмотреть, какая у меня сейчас версия хрома… Так хром взял, и с 49 обновил до 50-й...)
AleksDesker
Прочитал ваш коммент, пошел посмотреть, а какая у меня… обновил хром. Походу вы запустили цепную реакцию :)
khim
Хех. Обычно Хром «пропихивается» несколько дней, иногда — неделю или две и при этом смотрится — не началась ли «цепная реакция» (резко увеличилось количество крешей, например).
Так как речь идёт об ошибках проявляющихся в количества «штук на миллион», то «на кошках» такое не отловишь, увы… А проблемы могут быть реальными, так что сразу на всех выкатывать новую версию опасно…
Так и получается, что новая версия уже есть, но у вас её нет, так как вы стоите где-то в хвосте «очереди». Но если человек полез-таки смотреть какая у него версия, то, стало быть, ему это отчего-то важно. В этом случае его переставляют в начало оной очереди.
Так что эффект «пошел посмотреть, а какая у меня… обновил хром» — это не «совпадение», а «фича».
Kain_Haart
Semver предназначен для версионирования продукта, предоставляющего внешний api. Chrome и Firefox скорее являются продуктом для пользователей.
vlivyur
Разработчики расширений согласны?
encyclopedist
В курсе.
На самом деле они используют семантическое версионирование. Просто у таких проектов, которые состоят из очень большого числа компонентов и очень большого числа разных API каждая версия является мажорной. Потому что в каждой версии будет содержаться новое мажорное несовместимое изменение какого-нибудь компонента (то рендеринг HTML, то свойства CSS, то Javascript, то интерфейс, то ещё что-то).
То же самое касается операционных систем, (и их ядер, оттого и смена версионирования Линукса).
Семантическая версия плохо с