Немножко про модули в golang

· 5 min read

“Before software can be reusable, it first has to be usable.” – Ralph Johnson

Это статья скорее для себя, нежели чем хотелось чем-то поделиться. Идея её написания висела довольно давно, но сейчас сподвигло её написать статья “I want off Mr. Golang’s Wild Ride”. Мысленно можно разделить эту статью на две части: первая, в которой идёт обсуждение объединения или нормализации API между разными операционными системами, и вторая, в которой идёт речь про модули и их реализацию. Russ Cox на своём сайте сделал серию из 11 (!) статей, чтобы объяснить почему модули это не так просто, как казалось. Давайте кратко пройдёмся по истории работы с библиотеками в golang.

Ненавистный GOPATH

Первое, что тебе нужно было сделать прежде, чем начать работу с golang - это установить переменную GOPATH. Она была нужна для того, чтобы команда go get знала куда складывать пакеты. Сейчас есть значение по умолчанию равное $HOME/go, что немножко упрощает работу, но не сильно. В качестве неймспейса для всех библиотек используется формат host/namespace/lib, например: github.com/xo/usql. Пробуем поставить утилиту usql:

% time go get github.com/xo/usql
56.93s user 6.39s system 85% cpu 1:13.98 total

% tree -d -L 3 ~/go/src
├── github.com
│   ├── alecthomas
│   │   ├── chroma
│   │   ├── kingpin
│   │   ├── template
│   │   └── units
│   ├── danwakefield
│   │   └── fnmatch
│   ├── denisenkom
│   │   └── go-mssqldb
│   ├── dlclark
│   │   └── regexp2
│   ├── gohxs
│   │   └── readline
│   ├── golang-sql
│   │   └── civil
│   ├── go-sql-driver
│   │   └── mysql
│   ├── lib
│   │   └── pq
│   ├── mattn
│   │   ├── go-isatty
│   │   ├── go-runewidth
│   │   └── go-sqlite3
│   ├── xo
│   │   ├── dburl
│   │   ├── tblfmt
│   │   ├── terminfo
│   │   ├── usql
│   │   └── xoutil
│   └── zaf
│       └── temp
└── golang.org
    └── x
        ├── crypto
        └── sys

% du -h --max-depth=1 ~/go/src/
77M	./github.com
35M	./golang.org
111M	.

Утилита предназначена для работы с разными базами данных и ожидаемо ей требуется много библиотек-драйверов для работы, но 111 мегабайт и минута на скачивание выглядит, как перебор. Если вам нужна только сама утилита, всегда можно использовать опцию -u. В защиту такого подхода, хочу сказать что использование прямого соответствия между импортом библиотеки и пути по которому она находится удобно. Так как язык разработал Google, то сразу был сделан вывод о том, что вся эта система работала поверх монорепозитория внутри компании и такое использование было логично. Но как только язык вышел публично, сразу стало понятно что использовать хост сервиса в пути не самое удобное решение. В случае, если проект был перемещён в другое пространство, ничего страшного не происходит, но вот если поменялся хост, то труба. Об этом подумали несколько людей и началась вторая эпоха.

Средневековый vendor

Когда node.js только набирал популярность (версии 0.8-0.12), шёл довольно большой холивар на тему вендоринга зависимостей. Были люди, которые говорили, что нужно складывать всю папку node_modules в репозиторий и при обновлении комиттить изменения, но в итоге сообщество пришло к тому, что это приносит только лишние расходы и отказались. Как мы все потом знаем, это привело к трагедии Дарта Плегаса leftpad. В Golang решили пойти тем же путём и появилась папка vendor, правда чтобы её использовать необходимо было перезаписать GOPATH в текущую директорую и тогда начинались пляски с тем, что в директории должна была быть папка src, а в .gitignore шли директории bin/ и pkg/. Эту проблему надо было автоматизировать и появились инструменты, чтобы с таким подходом было работать удобно, а именно glider, а в последствии dep. Более того, dep был сделан как официальный эксперимент группы разработки языка, и в итоге именно он дал толчок развитию модулей в go 1.11. Из-за данных инструментов в репозиториях пропали папки vendor, так как они создавали lock-файлы и можно было воспроизвести среду. А вот проблемы были в том, что каждый инструмент делал свой формат. :(

Современный go.mod

И здесь мы возвращаемся к модулям с которых началась эта статья. В go 1.11 можно было с помощью переменной GO111MODULE=on, а с недавним релизом go 1.14 данное поведение стало по умолчанию. Что нужно сделать для того, чтобы его использовать прекрасно описано в The Go Blog в 4 (!) частях. Здесь бы хотелось сосредоточиться на том, почему это не современная штука.

Проблема №1. Перейти надо всем

Разработчики golang молодцы, что дали 3 минорные версии для того, чтобы авторы библиотек могли перейти на go.mod и судя по тому, что из 21000 открытых issues на гитхабе закрыто 18000 все были нацелены на использование данного инструмента.

Проблема №2. Формат mod в go.mod

Долго бомбило после прочтения PEP 518, что выбрали формат toml. Главное, что его выбрали потому что json слишком вербозный, а yaml имеет слишком сложную спецификацию. В golang решили просто пойти своим путём и сделать формат, который надо парсить. Можете пойти почитать, как он устроен тут. Уверен, что выбор был сделан исходя из принципа упрощения: зачем брать известное, если можно сделать своё простое? Более того, этот файл нельзя трогать руками. Всё взаимодействие с ним идёт через команду go get, так что частично мы откатились к прежнему решению с GOPATH, просто теперь все пакеты лежат в GOPATH/pkg.

Проблема №3. Версионирование

Все мы оказывались в ситуации, когда автор библиотеки не удосужился указать версии своих зависимостей и занимаешься бинарным поиском совместимых пакетов. В golang решили пойти дальше и сказать, что изменение мажорной версии должно приводить к изменении путей. Тема хорошая, но реализация полагается на рациональность людей, который будут эту мажорную версию менять. А как мы знаем, у этой проблему есть две крайности: Chrome,который имеет 80 версию на данный момент, и PostgresQL, который долгое время жил на 9.x номенклатуре и утверждал, что вторая цифра - это тоже мажорный релиз. Справился ли подход golang с тем, чтобы пакеты всегда собирались при обновлении пакетов? Нет, для примера вы можете взять обновление пакета grpc-consul-resolver из-за того, что в минорной версии сменился интерфейс, и это библиотека от google.

Проблема №4. Настройки

Ладно, тут я просто напишу GOSUMDB=off, GOPRIVATE=*example.com* и обязательно GO111MODULE=on.

Итого

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

P.s. а может быть и не с нуля, но осадочек остался.

P.p.s. кстати, с модулями не отказались от вендоринга, даже комманду сделали для этого go mod vendor.