Golang — архитектурный линтер
- воскресенье, 30 июля 2023 г. в 00:00:33
Для того чтобы повысить качество приложения, написанного на языке go, можно использовать разные линтеры. Один из таких линтеров — архитектурный.
В приложении архитектура — это то, как код разложен по «слоям», и какие слои могут вызывать друг друга.
В данной статье расскажу про свой бесплатный, open-source, линтер с MIT лицензией и чем он может быть полезен.
Линтер работает с любым кодом, не только с web-приложениями, но для примера посмотрим на web-приложение с одним АПИ методом:
GET /books?author=Joe
У приложения могут быть такие слои:
handler (тут обработчики API занимаются валидацией и трансформацией in/out данных)
service (бизнес-логика сервиса)
repository (инфровый слой для доступа к данным)
models (тут будут лежать domain DTO'шки)
Мы хотим, чтобы запросы в repository мог отправлять только service слой, но не мог handler, получается такая картина:
Такую схему зависимостей можно описать в виде yml файлика:
version: 3
workdir: internal
components:
handler: { in: handlers/* }
service: { in: services/* }
repository: { in: repositories/* }
models: { in: models/** }
commonComponents:
- models
deps:
handler:
mayDependOn:
- service
service:
mayDependOn:
- repository
p.s. к обозначению полей в этом файле вернемся чуть позже.
Для наглядности и простоты приложение будет выполнять 1 запрос прямо в main.go
и завершаться. Тут же будет сборка зависимостей.
func main() {
repository := booksRepository.NewRepository()
service := booksService.NewService(repository)
handler := booksHandler.NewHandler(service)
books, err := handler.Books("Joe")
if err != nil {
panic(err)
}
for _, book := range books {
fmt.Printf("Book %d has author %s\n", book.ID, book.Author)
}
os.Exit(0)
}
Сейчас приложение полностью соответствует описанной архитектуре, если запустить линтер и посмотреть результаты, получим ожидаемое «OK»:
$ go-arch-lint check
OK - No warnings found
Теперь можно поменять код так, чтобы он нарушал правила:
func main() {
// ..
repository := booksRepository.NewRepository()
handler := booksHandler.NewHandler(
service,
repository, // нарушаем правило тут!
)
// ..
}
И запустим ещё раз:
Тут мы видим проблемный код, где конкретно и что было нарушено, и можем это поправить.
С уже существующей кодовой базой есть одна проблема — в ней точно будет «грязный» код, который самым неявным способом нарушит «чистую» архитектуру.
Поэтому добавление линтера в проект должно происходить в несколько шагов:
Текущее состояние проекта
Добавление .go-arch-lint.yml файл с описанием идеальной архитектуры
Линтер находит проблемные места в проекте. Их не стоит исправлять сразу, а можно «легализовать» добавлением в конфиг и todo комментарием (можно с ссылкой на задачу в jira или каком-то трекере)
Асинхронно, в свободное время, тех. долг и т.п. можно спокойно исправить код
После исправлений осталось только подчистить граф зависимостей
Такой алгоритм позволит добавить линтер в существующий проект сразу, хоть мы пока ничего не улучшили, но зато теперь видим проблемы, и можем их исправить в будущем. При этом линтер не даст добавить новых проблем и уже этим приносит пользу.
Для большего качества можно проверять не только локальные зависимости в проекте, но и то, какие вендорные библиотеки используются в вашем коде.
Для начала нужно расширить конфиг:
переключить allow.depOnAnyVendor
в false
— теперь линтер будет проверять вендорные зависимости
добавить поле vendors
с описанием вендорных зависимостей
в поле commonVendors
можно указать вендорные либы, которых можно будет импортировать из любого места в проекте
в графе зависимостей поле canUse
отвечает за список вендорных зависимостей, которые этот компонент может использовать (импортировать)
version: 3
allow:
depOnAnyVendor: false
..
vendors:
company: { in: my-company.example.com/*/pkg/** }
validation: { in: github.com/go-ozzo/ozzo-validation }
observability:
in:
- github.com/prometheus/**
- github.com/uber-go/zap
- go.opentelemetry.io/otel
- go.opentelemetry.io/otel/*
commonVendors:
- company
- observability
deps:
handler:
mayDependOn:
- service
canUse:
- validation
Архитектурный линтер состоит из 3 частей:
Component — это абстракция над package (пакетом). Один компонент включает в себя один или более пакетов.
Dependency — зависимость. Они бывают двух видов: явные и неявные.
Явные зависимости — это import
в файле с описанием конкретной зависимости от другого пакета.
Неявные — это передача методов или структур с методами через интерфейсы, каналы и прочие способы
Dependency tree — граф отношений между компонентами (кому и от кого можно зависеть)
Для примера более сложная архитектура с такими слоями:
components:
handler: { in: handlers/* }
service: { in: services/** }
repository: { in: services/*/repository }
models:
in:
- services/*/domain
- services/*/dto
Если запустить маппинг, можем увидеть связь между компонентами и go пакетами:
$ go-arch-lint mapping
Package | Compoenent
----------------------------------------------
internal | -
handlers | -
articles | handler
auth | handler
books | handler
info | handler
user | handler
services | -
auth | -
logic | service
dto | models
repository | repository
books | -
somecode | service
domain | models
repository | repository
user | -
admin | service
groups | service
dto | models
repository | repository
При разметке несколько wildcard масок могут покрыть один и тот же package, в данном примере internal/services/auth/repository/example
покрывают 2 компонента:
service в services/**
repository в services/*/repository
Но каждый пакет может быть привязан только к одному компоненту. Линтер выберет компонент с самым точным описанием пути, который находит по маске меньше всего пакетов, или имеет более вложенную структуру.
размечает весь код на компоненты
находит все зависимости между компонентами
строит граф зависимостей
сравнивает актуальный и желаемый граф зависимостей
если мы получили непустой DIFF — значит, есть проблемы
Установка:
go install github.com/fe3dback/go-arch-lint@latest
Запуск:
cd code/acme/my-project
go-arch-lint check
Другие способы запуска и установки можно найти на странице проекта: https://github.com/fe3dback/go-arch-lint
Для более удобной работы с конфигом можно установить плагин: https://plugins.jetbrains.com/plugin/15423-goarchlint-file-support
Все команды линтера можно запускать с флагом `--json`. Это позволит просто получить любые данные по проблемам, проекту, схеме и т.п. что удобно для интеграции с другими тулзами, CI/CD утилитами и т.п.
go-arch-lint check --json
{
"Type": "models.Check",
"Payload": {
// ..
"ArchWarningsDeepScan": [
{
"Gate": {
"ComponentName": "handler",
"MethodName": "NewHandler",
"Definition": { .. }
},
"Dependency": {
"ComponentName": "repository",
"Name": "books.Repository",
"InjectionAST": "repository",
"Injection": {
"Valid": true,
"File": "/go/src/acme/my-project/main.go",
"Line": 15,
"Offset": 37
}
},
"Target": { .. }
}
],
// ..
}
}
Можно экспортировать граф зависимостей в виде svg файлика или d2 описания.
go-arch-lint graph
go-arch-lint graph --d2
handler -> service
service -> repository
Если у кого-нибудь есть желание написать плагин для других редакторов (к примеру vscode), в линтере есть доп. методы для удобства интеграций.
# Основная информация о проекте
go-arch-lint self-inspect --json
# Json-schema для валидации yaml конфига
go-arch-lint schema --version 3
# {"$schema":"http://json-schema.org/draft-07/schema#", ...
# Проверка кода проекта, результаты в json
go-arch-lint check --json
# Маппинг go пакетов на компоненты
go-arch-lint mapping --json
Полный список команд и флагов можно посмотреть в --help
go-arch-lint --help
Usage:
go-arch-lint [command]
Available Commands:
check check project architecture by yaml file
completion Generate the autocompletion script for the specified shell
graph output dependencies graph as svg file
help Help about any command
mapping mapping table between files and components
schema json schema for arch file inspection
self-inspect will validate arch config and arch setup
version Print go arch linter version
Flags:
-h, --help help for go-arch-lint
--json (alias for --output-type=json)
Если есть желание помочь с разработкой, можно посмотреть на код в репозитории.