Toolset: Manage project-specific tools
TLDR#
Toolset - менеджер инструментов разработки для Go проектов. Решает проблему “у меня работает, а в CI не собирается” через изоляцию и версионирование инструментов на уровне проекта.
Проблема: почему инструменты должны быть версионированы#
Представьте ситуацию: вы пушите код, локально все тесты проходят, линтер молчит. Но в CI сборка падает с ошибкой линтера. Знакомо? Проблема в том, что у вас локально golangci-lint v1.59.0
, а в CI - v1.62.0
, и новая версия нашла проблему, которую старая пропускала.
Или другой сценарий: новый разработчик клонирует репозиторий, запускает task lint
и получает совсем другие ошибки, чем видят остальные. Оказывается, у него установлена другая версия инструмента.
Почему это важно#
- Воспроизводимость: Одинаковый код должен давать одинаковые результаты проверок на всех машинах
- Предсказуемость: Обновление инструмента должно быть осознанным решением, а не сюрпризом
- Командная работа: Все разработчики должны использовать одни и те же версии инструментов
Что нужно версионировать#
В типичном Go проекте это:
golangci-lint
- линтер с десятками анализаторов внутриgofumpt
/goimports
- форматирование кодаmockgen
/moq
- генерация моковsqlc
/jet
- генерация кода для работы с БДbuf
/protoc-gen-go
- работа с protobuf- И десятки других специализированных инструментов
Существующие подходы и их ограничения#
Рассмотрим, как обычно решают эту проблему и почему каждый подход имеет свои недостатки:
1. Глобальная установка (антипаттерн)#
brew install golangci-lint
# Или
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
Проблемы:
- Версия не фиксирована в репозитории
- Конфликты между проектами (проект A требует v1.59, проект B - v1.62)
- “Работает у меня” != “работает у всех”
- Сложно синхронизировать версии в команде
2. Подход с tools.go (официальная рекомендация Go)#
Другой способ - создать в проекте файл tools.go
(назвать можно как угодно) с примерно таким содержанием и выполнить
go get ./...
//go:build tools
// +build tools
package tools
import (
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "github.com/go-jet/jet/v2/cmd/jet"
_ "github.com/vburenin/ifacemaker"
)
Это официальный подход Go для управления инструментами. Версии фиксируются в go.mod
.
Проблемы на практике:
Конфликты зависимостей:
myproject требует github.com/stretchr/testify v1.8.0 golangci-lint требует github.com/stretchr/testify v1.9.0
Go выберет v1.9.0, что может сломать ваши тесты.
Загрязнение go.mod:
- Инструмент sqlc тянет 50+ зависимостей
- Ваш security scanner начинает ругаться на уязвимости в CLI инструментах
- Renovate/Dependabot создает PR на обновление зависимостей инструментов
Неудобство использования:
go mod download
# Создаем директорию, которая будет содержать все установленные программы для этого проекта
mkdir tools
echo "tools" >> .gitignore
# Указываем директорию, в которую будут устанавливаться все инструменты
export GOBIN=$(pwd)/tools
# Выполняем установку без указания версий - они указаны в go.mod
go install github.com/golangci/golangci-lint/cmd/golangci-lint
go install github.com/go-jet/jet/v2/cmd/jet
go install github.com/vburenin/ifacemaker
Документация про GOBIN для получения деталей.
После установки - все программы окажутся в директории ./tools
и мы сможем использовать их напрямую. Проверим
$ ./tools/golangci-lint version
golangci-lint has version v1.62.2...
$ ./tools/jet -help | head -1
Jet generator v2.11.1
- Проблемы с go run:
# Каждый раз(ну почти) компилирует заново - медленно! go run github.com/golangci/golangci-lint/cmd/golangci-lint run
3. Скрипты и Makefile#
Многие проекты создают скрипты установки:
# Makefile
GOLANGCI_VERSION = v1.62.2
MOCKGEN_VERSION = v1.6.0
.PHONY: install-tools
install-tools:
@mkdir -p ./bin
@GOBIN=$(PWD)/bin go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_VERSION)
@GOBIN=$(PWD)/bin go install github.com/golang/mock/mockgen@$(MOCKGEN_VERSION)
Проблемы:
- Версии разбросаны по Makefile
- Сложно проверять обновления
- Каждый проект изобретает свой велосипед
4. Docker контейнеры#
Можно завернуть все инструменты в Docker образ (или образы) и запускать их в проекте, монтируя директорию проекта в рабочую директорию контейнера. Некоторые инструменты даже поставляют сборки. Например, golangci-lint. Можно было бы собрать все инструменты в одном образе примерно вот так:
FROM golang:1.23-alpine3.21
RUN mkdir /src
WORKDIR /src
ENV VERSION_GOLANGCI='v1.62.0'
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@${VERSION_GOLANGCI}
ENV VERSION_JET='v2.11.1'
RUN go install github.com/go-jet/jet/v2/cmd/jet@${VERSION_JET}
ENV VERSION_IFACEMAKER='v1.2.1'
RUN go install github.com/vburenin/ifacemaker@${VERSION_IFACEMAKER}
Идея: упаковать все инструменты в Docker образ.
Реальные проблемы:
- Скорость: Docker overhead на каждый запуск
- Интеграция с IDE: VSCode/GoLand не могут использовать инструменты из контейнера
- Сложность с приватными репозиториями: нужно пробрасывать SSH ключи
- Размер: образ с 5-10 инструментами может весить гигабайты
- Обновления: пересборка образа при каждом обновлении
5. Встроенная поддержка в Go 1.24+#
В Go 1.24 появилась экспериментальная поддержка:
go get -tool golang.org/x/tools/cmd/stringer
go tool stringer # запуск
Ограничения:
- Все те же проблемы с конфликтами зависимостей
- Нет группировки инструментов
- Нет изоляции между проектами
- Зависимости инструментов влияют на зависимости приложения. Могут быть конфликты
Toolset: как должно работать управление инструментами#
toolset - менеджер инструментов, который решает большую часть описанных проблем.
Ключевые принципы#
- Изоляция: Каждый проект имеет свой набор инструментов, без конфликтов
- Декларативность: Все инструменты описаны в
.toolset.json
- Простота: Одна команда для установки всего
- Скорость: Предкомпилированные бинарники, без overhead
- Гибкость: Теги, версии runtime, композиция конфигураций
Реальный пример использования#
Покажу на примере типичного Go проекта:
1. Установка toolset#
# Один раз глобально
go install github.com/kazhuravlev/toolset@latest
2. Инициализация проекта#
# В корне вашего проекта
toolset init .
# Создается .toolset.json и .toolset.lock.json
3. Добавление инструментов#
# Линтеры и форматтеры
toolset add --tags lint,ci go github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0
toolset add --tags format go mvdan.cc/gofumpt@latest
# Генераторы кода
toolset add --tags generate go github.com/golang/mock/mockgen@v1.6.0
toolset add --tags generate go github.com/sqlc-dev/sqlc/cmd/sqlc@v1.20.0
# Инструменты для конкретной версии Go
toolset runtime add go@1.21.5
toolset add --tags legacy go@1.21.5 github.com/old/tool@v1.0.0
После добавления .toolset.json
выглядит так:
{
"tools": [
{
"type": "go",
"path": "github.com/golangci/golangci-lint/cmd/golangci-lint",
"version": "v1.62.0",
"tags": ["lint", "ci"]
},
{
"type": "go",
"path": "github.com/golang/mock/mockgen",
"version": "v1.6.0",
"tags": ["generate"]
}
]
}
4. Установка и использование#
# Установить все
toolset sync
# Или только для CI
toolset sync --tags ci
# Запуск инструментов
toolset run golangci-lint run ./...
toolset run mockgen -source=interface.go -destination=mock.go
# Или добавьте в PATH
export PATH="$(pwd)/bin/tools:$PATH"
golangci-lint run ./...
5. Интеграция в CI#
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install toolset
run: go install github.com/kazhuravlev/toolset@latest
- name: Install CI tools
run: toolset sync --tags ci
- name: Run linters
run: toolset run golangci-lint run ./...
Дополнительные возможности#
Композиция конфигураций#
Можно создать базовую конфигурацию для всей компании:
# Базовая конфигурация в отдельном репозитории
toolset add --include git+https://github.com/company/base-tools.git
# Добавить специфичные для проекта
toolset add go github.com/project-specific/tool
Управление неиспользуемыми инструментами#
# Найти инструменты, которые давно не использовались
toolset list --unused
# Почистить
toolset remove unused-tool
Разные версии Go для разных инструментов#
# Старый инструмент требует Go 1.19
toolset runtime add go@1.19.13
toolset add go@1.19.13 github.com/legacy/tool@v1.0.0
# Новый использует последнюю версию
toolset add go github.com/modern/tool@latest
Итоги#
Toolset решает реальную проблему Go разработчиков - управление версиями инструментов. Он:
- Воспроизводимые наборы инструментов - все используют одинаковые версии
- Упрощает onboarding -
toolset sync
и все готово - Не конфликтует с проектом - инструменты изолированы от go.mod
- Интегрируется везде - CI, pre-commit hooks, IDE