Skip to content

About

Architecture linter for Go: import rules, tiers, deep-scan method calls and dependency injection — declared as code in a Go DSL spec (YAML legacy), checked on every CI run. Hexagonal/onion/DDD-friendly.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

368 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Русский | English


Logo image

Линтер архитектуры для Go (go-arch-lint): описываете слои и зависимости на type-safe Go DSL — линтер находит нарушения в импортах и инъекциях зависимостей. Подходит для clean architecture, hexagonal, onion и DDD.

CI Go Report Card Go Reference Go Version License: MIT Release go-recipes

Установка

go install github.com/vsfedorenko/go-arch-lint/v3/cmd/arch-lint@latest

Или через Docker:

docker run --rm -v ${PWD}:/app ghcr.io/vsfedorenko/go-arch-lint:latest check --project-path /app

Или бинарник из релизов.

Путь модуля — github.com/vsfedorenko/go-arch-lint/v3 (суффикс обязателен для мажорных версий Go-семвера). Если вы обновляетесь с v2.x: замените импорты в .go-arch-lint/ на /v3 и мигрируйте спеку (см. «Миграция с v2.x» ниже). Проще начать с нуля: rm -rf .go-arch-lint && go-arch-lint init сгенерирует свежую спеку со всеми каталогами проекта (init не перезаписывает существующий каталог, а v2-спека всё равно несовместима).

Примеры: гексагональная и чистая архитектуры

Спека — это дерево зависимостей, записанное в порядке «зависимости раньше». Два канонических шаблона:

Гексагональная (ports & adapters)

cmd/
└── app/               # точка входа: собирает всё
internal/
├── domain/            # сердце: сущности, ноль внешних зависимостей
├── core/              # логика приложения: использует только domain
└── adapter/           # периферия: доставляет запросы в core
    ├── http/
    └── db/
var build = Spec(func() {
	domain := Path("internal/domain")

	core := Path("internal/core", func() { Use(domain) })

	http := Path("internal/adapter/http", func() { Use(core) })
	db := Path("internal/adapter/db", func() { Use(core, domain) })

	Path("cmd", func() {
		Path("app", func() { Use(core, db, http) }) // точка входа собирает всё
	})
})

Стрелки смотрят внутрь: адаптеры знают про core, core — только про domain. Импорт domain из adapter/http проверка забракует: в правиле Use его нет.

Чистая архитектура (Clean)

internal/
├── entity/        # Enterprise-правила: сущности
├── usecase/       # сценарии приложения
├── adapter/       # интерфейсы наружу: repo, web
│   ├── repo/
│   └── web/
└── framework/     # драйверы: БД, http-сервер
var build = Spec(func() {
	entity := Path("internal/entity")

	usecase := Path("internal/usecase", func() { Use(entity) })

	repo := Path("internal/adapter/repo", func() { Use(usecase, entity) })
	web := Path("internal/adapter/web", func() { Use(usecase, entity) })

	pg := Vendor("pgx", "github.com/jackc/pgx/v5")
	chi := Vendor("chi", "github.com/go-chi/chi/v5")

	Path("internal/framework", func() { Use(repo, web, usecase, entity, pgx, chi) })

	Path("cmd", func() {
		Path("app", func() { Use(usecase, entity, repo, web, pgx, chi) })
	})
})

Вендоры (pgx, chi) живут в одном внешнем слое — framework и точка входа; внутренние слои не знают о них ничего. Попытка импортировать pgx из usecase — нарушение на файл:строке.

Все три примера — живые проекты в examples/ (basic — слои, ddd — bounded contexts, hexagonal — порты и адаптеры), и CI проверяет каждый: job examples прогоняет go-arch-lint check по всем трём на каждом PR.

Требования

  • Go 1.25+ (поддерживаются две последние мажорные версии: 1.25 и 1.26; CI тестирует обе).
  • go должен быть доступен на PATH — CLI компилирует .go-arch-lint/arch.go через go run (кэшируется, повторные запуски ~2s).
  • Go workspaces (go.work). Проект с корневым go.mod работает полностью — включая соседние модули-члены workspace: init исключает их из архитектуры по умолчанию, а при желании их можно объявить компонентами (Path("two/y")) и писать кросс-модульные правила Use. Модуль-член может иметь любой собственный module path (example.com/y, не обязательно example.com/root/two/y) — импорты членов классифицируются как project-код, а не vendor. Workspace из модулей-сиблингов без корневого go.mod пока не поддерживается — CLI подскажет это при запуске.

Конфигурация

Конфигурация — это Go-файл. Не YAML, не JSON — обычный Go-код с типобезопасностью и автодополнением в IDE.

cd ~/code/my-project
go-arch-lint init

Создаёт .go-arch-lint/ с тремя файлами. Спека и раннер разделены: arch.go — ваш файл, main.go — служебный, его трогать не нужно:

// arch.go — здесь вы описываете архитектуру
package main

import (
	. "github.com/vsfedorenko/go-arch-lint/v3/dsl"
)

var build = Spec(func() {
	// Каждый каталог с Go-кодом объявлен, и каждый существующий
	// импорт разрешён: каркас зеркалит код как есть. Ужесточайте
	// правила Use по мере роста архитектуры:
	//
	//     domain := Path("internal/domain")
	//     Path("internal/core", func() { Use(domain) })
	domain := Path("internal/domain")
	handlers := Path("internal/handlers", func() { Use(domain) })
	Path(".", func() { Use(domain, handlers) })
	Path("cmd/app", func() { Use(domain, handlers) })
})
// main.go — сгенерирован, не редактируйте: пробрасывает флаги CLI в проверку
package main

import (
	"os"

	"github.com/vsfedorenko/go-arch-lint/v3"
)

func main() {
	archlint.MustRunCLI(build, os.Args[1:])
}

Что здесь происходит:

— init сканирует проект и объявляет каждый каталог с Go-кодом как компонент (Path), каждый внутренний импорт становится правилом Use, а каждая сторонняя библиотека — декларацией Vendor + Use — свежий каркас зеркалит код как есть и сразу проходит проверку. Ужесточить правило — значит удалить строку Use и посмотреть, какие нарушения всплывут. — Use — единственное правило: «этот путь использует эти цели». Без явного Use запрещено всё. — Vendor(name, import) — внешняя библиотека как легальная цель; стандартная библиотека разрешена всегда. — Порядок объявления = направление зависимостей: сослаться вперёд нельзя, это ошибка компиляции Go.

Полный список функций DSL — в документации синтаксиса или через go doc github.com/vsfedorenko/go-arch-lint/v3/dsl.

Готовые шаблоны архитектур

Не хотите начинать с чистого листа — возьмите пример из каталога examples/: скопируйте .go-arch-lint/ из basic (слои), ddd (bounded contexts) или hexagonal (порты и адаптеры) в свой проект и поправьте пути. Каждый пример — законченный проект со спекой на актуальном DSL.

Флаг --recipe существовал в v1/v2 и удалён в v3: init больше не принимает ни его, ни других неизвестных флагов — молчаливый откат к дефолтной спеке был бы хуже ошибки.

Проверка

go-arch-lint check

Линтер строит граф импортов из реального кода, сравнивает с графом из конфигурации и выводит нарушения:

Check output

Код возврата Значение
0 Нарушений нет
1 Найдены нарушения
2 Сломана конфигурация: arch.go не компилируется или проект не читается

Код 2 означает «проверка не выполнялась» — чините конфигурацию, а не код.

Флаг --json переключает вывод в машиночитаемый формат для CI.

Подавление известных нарушений (baseline)

Для инкрементального внедрения на legacy-коде известное нарушение можно пометить в исходнике директивой — проверка пройдёт, а новые нарушения по-прежнему будут фейлить сборку:

//go-arch-lint:ignore            // подавить любое нарушение на строке
//go-arch-lint:ignore beta       // только если цель зависимости — beta
import _ "example.com/app/internal/beta"

//go-arch-lint:ignore-file       // подавить все нарушения в файле
package legacy

Директива ставится на самой строке нарушения или строкой выше. Аргумент (через пробел, можно несколько) фильтрует по цели: имени компонента (beta) или последнему сегменту импорт-пути (internal/beta → beta). Подавленные нарушения не исчезают бесследно: в конце отчёта выводится suppressed: N (by //go-arch-lint:ignore directives), а в JSON — поле SuppressedCount, так что технический долг остаётся видимым.

Baseline-файл: инкрементальное внедрение

Когда нарушений сотни и размечать исходники директивами нереалистично, запишите их в baseline-файл — проверка будет падать только на НОВЫХ нарушениях («не чинить всё сразу, но и не добавлять нового»):

# 1. Записать текущие нарушения (файл коммитится в репозиторий):
go-arch-lint check --baseline .go-arch-lint/baseline.json --baseline-update

# 2. В CI — только сравнение: известный долг толерируется,
#    новое нарушение фейлит сборку (exit 1):
go-arch-lint check --baseline .go-arch-lint/baseline.json

Baseline — JSON с версией схемы и отпечатками (fingerprints) нарушений: kind|rule|file без номеров строк, поэтому правки выше по файлу не «воскрешают» старый долг как новый. Сводка в текстовом выводе показывает baseline: N new, M known (tolerated), в JSON — поля BaselineNewCount и BaselineKnownCount. Отсутствующий baseline-файл в режиме сравнения — ошибка конфигурации (exit 2), а не молчаливый пропуск. Когда долг починен — просто перезапишите baseline (устаревшие отпечатки игнорируются).

Под капотом команды check/mapping/graph/explain/selfInspect делегируются в .go-arch-lint/ через go run — как устроена маршрутизация флагов, выходные коды и кэширование (холодный старт ~45 с, рабочий режим ~2 с), см. docs/delegation.md.

Граф зависимостей

go-arch-lint graph --format=mermaid
graph LR
  handler --> service
  service --> repository
  handler -.-> n0["3rd-cobra"]

Четыре формата вывода:

--format Куда Зачем
svg файл (по умолчанию) готовое изображение
d2 stdout исходник d2 для ручной доработки
plantuml stdout рендер через PlantUML или CI
mermaid stdout Markdown, GitHub, GitLab

Дополнительно: --type=di (обратный граф, DI), --focus=handler (только один компонент), --include-vendors (показать сторонние библиотеки).

Программный API

go-arch-lint — не только CLI, но и библиотека. Спека описывается Path-DSL — весь язык умещается в четыре вызова:

import (
	"github.com/vsfedorenko/go-arch-lint/v3"
	. "github.com/vsfedorenko/go-arch-lint/v3/dsl"
)

func runArchCheck() error {
	build := Spec(func() {
		domain := Path("shop/domain")
		pgx := Vendor("pgx", "github.com/jackc/pgx/v5")

		Path("shop/core", func() {
			Use(domain, pgx) // core использует domain и pgx
		})
	})

	return archlint.Run(build, archlint.WithProjectPath("."))
}

Обе формы равнозначны: точечный импорт с Spec(func() {...}) — компактная, а явная Spec(func(s *dsl.SpecBuilder) {...}) с методами — без глобального роутинга. Стили можно миксовать в одной спеке.

Правила простые:

  • Path("a/b") — каталог с Go-кодом, то есть компонент. Path("a/b/**") — всё поддерево как один компонент. Path(".") — корень модуля (пакет в корне, без подкаталогов).
  • Use(...) — единственное правило: «этот путь использует эти цели». Цели — только Path/Vendor переменные, миксуются свободно. Импорты внутри объявленного пути к его собственным подкаталогам всегда разрешены.
  • Vendor(name, imports...) — внешний пакет (можно несколько путей на имя), легальная цель для Use. Стандартная библиотека (fmt, strings, …) разрешена всегда и Vendor не требует.
  • Exclude(paths...) —glob-пути вне архитектуры: тестовые фикстуры, examples, вложенные модули. Необъявленный в Path и не исключённый каталог — ошибка not attached to any component.
  • По умолчанию запрещено всё, что явно не разрешено Use.

Порядок объявления = направление зависимостей: сослаться вперёд нельзя, это ошибка компиляции Go. Опечатки и некорректные спеки паникуют при сборке с file:line. Каталоги проверяются по диску: несуществующий путь — ошибка конфигурации с подсказкой «did you mean».

archlint.MustRun(build) — то же, но с конвенциональными кодами возврата: 1 при нарушениях, 2 при ошибке конфигурации (см. archlint.ExitCode).

archlint.RunCLI(build, os.Args[1:]) — точка входа для скаффолда .go-arch-lint/main.go: маршрутизирует делегированные команды (check, mapping, graph, explain, self-inspect) по их собственному поведению, а не молча выполняет check. Диалект лаунчера (-p, --no-colors, selfInspect) переводится автоматически; запуск без команды по умолчанию — check. MustRunCLI — вариант с конвенциональными кодами возврата.

Миграция с v2.x

Первое поколение DSL (Version/Workdir/Component/Deps/CommonComponents) удалено. Механическая замена:

v2.x (удалено) v3
Component("n", "a/b") n := Path("a/b")
Deps("n", …MayDependOn("m")) Path("a/b", func() { Use(m) })
CommonComponents("n") Use(n) у каждого, кому нужен
AnyProjectDeps(true) перечислить цели явно в Use
Vendor(name, imp) (один путь) s.Vendor(name, imp1, imp2)
Workdir("internal") убрать: пути в Path — от корня модуля

Проще всего — перезапустить go-arch-lint init: он сгенерирует спеку со всеми каталогами проекта. RunV2/MustRunV2/RunCLIV2/MustRunCLIV2 переименованы в Run/MustRun/RunCLI/MustRunCLI. Naming, Tiers, Visibility, Interfaces правила первого поколения в v3 не входят — кандидаты на возврат отдельными расширениями.

Команды

Команда Назначение
init Создать каркас .go-arch-lint/ (единственный флаг — --project-path/-p; шаблоны архитектур — в examples/)
check Проверить архитектуру
explain Объяснить, как спецификация относится к одному импорту
graph Сгенерировать граф зависимостей
mapping Показать соответствие пакетов и компонентов
selfInspect Проверить архитектуру самого go-arch-lint
version Вывести версию (version, --version, -v)

Глобальные флаги: --project-path (кратко -p), --max-warnings N (лимит показа нарушений, по умолчанию 512; код выхода отражает полное число — подробности в docs/json-schema.md), --format text|json|sarif|junit|github-actions|html (check), --baseline <file> + --baseline-update (инкрементальное внедрение: известные нарушения толерируются, проверка падает только на новых — см. Baseline / инкрементальный режим), --output-type (ascii/json; неизвестное значение — ошибка конфигурации), --json (сокращение для --output-type=json), --output-json-one-line (однострочный JSON; без json-вывода — ошибка конфигурации, а не молчаливое игнорирование), --output-color / --no-colors (выключить ANSI-цвета).

Правила видимости (export visibility)

Visibility(func(){ VisibleTo(...) }) — ограничивает, какие компоненты могут потреблять API указанной компоненты:

Visibility(func() {
    VisibleTo("services")                    // полностью внутренняя: никто не может импортировать
    VisibleTo("models", "services", "container") // только services и container
})
  • Правило работает по фактическому импорт-графу: импорт компоненты вне allow-листа — нарушение
  • Сама компонента всегда может использовать свой код (self неявно разрешён)
  • Несколько правил на одну компоненту аккумулируются (allow-листы объединяются)
  • Сообщение указывает файл-свидетель первого импорта и видимый список

Размещение интерфейсов (порты с потребителем)

В гексагональной архитектуре интерфейс-порт живёт с потребителем, а не рядом с реализацией. Правило включается одной декларацией:

Interfaces(func() {
	MustLiveWithConsumer()
})

Чекер находит интерфейсы, которые использует ровно один другой компонент, и требует перенести декларацию к потребителю:

interface 'Iface' must live with its consumer 'alpha' (declared in component 'beta')
  • Интерфейс, используемый двумя и более компонентами, — законно общий, остаётся где объявлен.
  • Использование внутри своего компонента не считается.
  • Анализ чисто синтаксический (один полный проход парсера, без загрузки типов) — быстро и без сети.
  • Собственная спека go-arch-lint включает это правило.

Конвенции именования пакетов

Бесмысленные имена пакетов (utils, helpers, common, …) — свалки, в которые со временем утекает весь код. Правило включается одной декларацией в спеке:

Naming(func() {
	ForbiddenPackages("utils", "helpers", "common", "misc", "stuff")
})

Чекер сверяет имена всех просканированных пакетов проекта (фактические package X в файлах, не пути) и репортит каждое нарушение один раз на пакет с числом файлов:

Package name utils is forbidden internal/utils (3 file(s)): first at internal/utils/a.go

В JSON-выводе (--format json) нарушения имеют тип naming. Собственная спека go-arch-lint уже баннит utils, helpers, common, misc, stuff.

Метрики связанности (mapping)

mapping -s grouped показывает для каждого компонента метрики по фактическому графу импортов:

  services:
    coupling: out 3 | in 2 | stability 0.60
  • out (Ce) — на сколько компонентов компонент ссылается (fan-out);
  • in (Ca) — сколько компонентов ссылаются на него (fan-in);
  • stability — I = Ce / (Ca + Ce) по Роберту Мартину: 0 — максимально стабильный (на него опираются, сам ни на кого не завязан), 1 — максимально нестабильный. Ценные для переиспользования компоненты должны иметь низкий stability.

В JSON-выводе метрики приходят в MappingGrouped[].Coupling (omitempty — для компонентов без зависимостей поле отсутствует).

Explain: почему импорт запрещён

go-arch-lint explain <import> разбирает один путь импорта против текущей спецификации — команда отвечает на вопрос «почему check ругается (или не ругается) на этот импорт»:

$ go-arch-lint explain github.com/external/dep/pkg
Import: github.com/external/dep/pkg
Type:   vendor
Module: example.com/m
Verdicts (component -> decision, rule):
    DENY  internal/core — no matching vendor rule: the import is not covered by any Vendor used by this component
        fix: declare the dependency and allow it: vendor := Vendor("<name>", "github.com/external/dep/pkg") in arch.go, then Use(vendor) inside this component's Path(...)
    DENY  internal/util — no matching vendor rule: the import is not covered by any Vendor used by this component
        fix: declare the dependency and allow it: vendor := Vendor("<name>", "github.com/external/dep/pkg") in arch.go, then Use(vendor) inside this component's Path(...)
Usages (actual import sites):
    /app.go:6 ()

Выводит:

  • классификацию импорта (std / project / vendor) — ровно ту же, которой пользуется check;
  • владеющую компоненту (для project-импортов);
  • вердикт allow/deny для каждой объявленной компоненты с точным правилом, которое его породило, и конкретным фиксом для запрета (Vendor(...) + Use(...) в arch.go);
  • фактические места использования (файл:строка, компонента), до 20 — остальные считаются в OmittedUsages.

Вердикты выдаёт тот же код, что работает в check (единая точка решения VerdictForImport) — объяснение не может разойтись с тем, что реально проверяется. С --format json возвращает полную модель (ImportType, OwnerComponent, Verdicts[], Usages[], OmittedUsages) для CI и редакторных интеграций.

JSON-вывод для CI

check --format json печатает плоский массив нарушений (стабильный порядок, схема и примеры интеграции — в docs/json-schema.md): GitHub Actions-аннотации, GitLab CI-artifacts, семантика exit-кодов.

Для сканеров кода (GitHub Code Scanning, DefectDojo, SARIF-дашборды) есть check --format sarif — лог SARIF 2.1.0 с ruleId, уровнями и координатами нарушений: docs/json-schema.md → SARIF.

Для CI-дашбордов тестов (GitLab CI test reports, Jenkins JUnit plugin, Buildkite) есть check --format junit — JUnit-отчёт в XML: одно нарушение = один упавший testcase, у чистого проекта один зелёный arch-check. Рецепт: docs/json-schema.md → JUnit.

Для людей и архивов есть check --format html — самодостаточный HTML-отчёт (инлайн-CSS, без скриптов и внешних ассетов): карточки счётчиков по типам правил, таблица нарушений с file:line, экранирование враждебных путей, тёмная тема по prefers-color-scheme. Артефакт CI открывается напрямую в браузере. Схема: docs/json-schema.md → HTML.

Для pull request'ов есть официальный GitHub Action: аннотации ::error прямо на строках диффа, установка бинарника из релиза — без JS-скриптов:

- uses: actions/checkout@v7
- uses: actions/setup-go@v7
  with: { go-version: '1.25.13' }
- uses: vsfedorenko/go-arch-lint@v3 # moving major tag: always the latest v3.x

Подробности и все inputs: docs/json-schema.md → GitHub Action.

Коды выхода (check)

Код Значение
0 Нарушений нет
1 Найдены нарушения архитектуры
2 Ошибка конфигурации/системы (спека не собирается, проект не читается)

CI может различать «нашли нарушения» (падение сборки) и «сломан конфиг» (конфиг ничего не проверяет — звать мейнтейнера).

Примеры

В каталоге examples/ — три демонстрационных проекта:

  • basic — слоистая архитектура (handler → service → repository).
  • ddd — domain-driven design (domain → application → infrastructure → interfaces).
  • hexagonal — ports and adapters (core → adapters → domain).

Каждый пример содержит .go-arch-lint/ с конфигурацией arch-lint (arch.go + main.go).

Принцип работы

How is working

Линтер сопоставляет Go-пакеты с компонентами по glob-шаблонам, извлекает импорты из AST, строит фактический граф зависимостей и сравнивает его с желаемым графом из конфигурации. Несовпадения — это нарушения архитектуры.

Режим deep scan анализирует вызовы методов и инъекции зависимостей — не только импорты, но и структурное использование типов между компонентами.

Лицензия

MIT. Форк проекта go-arch-lint © fe3dback.

About

Architecture linter for Go: import rules, tiers, deep-scan method calls and dependency injection — declared as code in a Go DSL spec (YAML legacy), checked on every CI run. Hexagonal/onion/DDD-friendly.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages