TLDR#

Toolset - менеджер инструментов разработки для Go проектов. Решает проблему “у меня работает, а в CI не собирается” через изоляцию и версионирование инструментов на уровне проекта.

Проблема: почему инструменты должны быть версионированы#

Представьте ситуацию: вы пушите код, локально все тесты проходят, линтер молчит. Но в CI сборка падает с ошибкой линтера. Знакомо? Проблема в том, что у вас локально golangci-lint v1.59.0, а в CI - v1.62.0, и новая версия нашла проблему, которую старая пропускала.

Или другой сценарий: новый разработчик клонирует репозиторий, запускает task lint и получает совсем другие ошибки, чем видят остальные. Оказывается, у него установлена другая версия инструмента.

Почему это важно#

  1. Воспроизводимость: Одинаковый код должен давать одинаковые результаты проверок на всех машинах
  2. Предсказуемость: Обновление инструмента должно быть осознанным решением, а не сюрпризом
  3. Командная работа: Все разработчики должны использовать одни и те же версии инструментов

Что нужно версионировать#

В типичном 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.

Проблемы на практике:

  1. Конфликты зависимостей:

    myproject требует github.com/stretchr/testify v1.8.0
    golangci-lint требует github.com/stretchr/testify v1.9.0
    

    Go выберет v1.9.0, что может сломать ваши тесты.

  2. Загрязнение go.mod:

    • Инструмент sqlc тянет 50+ зависимостей
    • Ваш security scanner начинает ругаться на уязвимости в CLI инструментах
    • Renovate/Dependabot создает PR на обновление зависимостей инструментов
  3. Неудобство использования:

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
  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 образ.

Реальные проблемы:

  1. Скорость: Docker overhead на каждый запуск
  2. Интеграция с IDE: VSCode/GoLand не могут использовать инструменты из контейнера
  3. Сложность с приватными репозиториями: нужно пробрасывать SSH ключи
  4. Размер: образ с 5-10 инструментами может весить гигабайты
  5. Обновления: пересборка образа при каждом обновлении

5. Встроенная поддержка в Go 1.24+#

В Go 1.24 появилась экспериментальная поддержка:

go get -tool golang.org/x/tools/cmd/stringer
go tool stringer  # запуск

Ограничения:

  • Все те же проблемы с конфликтами зависимостей
  • Нет группировки инструментов
  • Нет изоляции между проектами
  • Зависимости инструментов влияют на зависимости приложения. Могут быть конфликты

Toolset: как должно работать управление инструментами#

toolset - менеджер инструментов, который решает большую часть описанных проблем.

Ключевые принципы#

  1. Изоляция: Каждый проект имеет свой набор инструментов, без конфликтов
  2. Декларативность: Все инструменты описаны в .toolset.json
  3. Простота: Одна команда для установки всего
  4. Скорость: Предкомпилированные бинарники, без overhead
  5. Гибкость: Теги, версии 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 разработчиков - управление версиями инструментов. Он:

  1. Воспроизводимые наборы инструментов - все используют одинаковые версии
  2. Упрощает onboarding - toolset sync и все готово
  3. Не конфликтует с проектом - инструменты изолированы от go.mod
  4. Интегрируется везде - CI, pre-commit hooks, IDE