DSL на Go: превращаем ваш код в новый язык
Представьте, что вы пишете систему обработки заказов. Каждый день менеджеры просят: «Добавьте скидку 10% для всех клиентов из Москвы, но только если сумма заказа больше 5000». Вы лезете в код, правите if-else, тестируете, деплоите. Через неделю — новая просьба: «Скидка 15% для VIP, если товаров больше трёх». Знакомо?
А что, если бы менеджеры сами могли писать такие правила? Без вашего участия. Без риска сломать продакшн. Именно для этого и существуют предметно-ориентированные языки (DSL) — компактные языки, заточенные под конкретную задачу. И Go, с его простотой и производительностью, идеально подходит для создания таких DSL.
В этой статье мы разберём, как создавать DSL на Go — от простых встраиваемых решений до полноценных внешних парсеров. Вы узнаете, как автоматизировать рутину и дать пользователям инструмент, который они поймут без программирования.
Что такое DSL и зачем он нужен в Go?
DSL (Domain-Specific Language) — это язык программирования, созданный для узкой области. В отличие от универсальных языков (Go, Python, Java), DSL решает одну задачу, но делает это элегантно.
Примеры из жизни:
- SQL — DSL для работы с базами данных.
- Regex — DSL для поиска шаблонов в тексте.
- HTML — DSL для разметки веб-страниц.
В Go DSL используются для:
- Конфигурации — вместо YAML/JSON можно писать логику на своём языке.
- Бизнес-правил — дать аналитикам возможность менять логику без программистов.
- Тестирования — описывать сценарии в читаемом виде.
- Обработки данных — создавать пайплайны трансформаций.
Go выбран не случайно: его компилятор быстр, синтаксис лаконичен, а встроенные инструменты (например, text/scanner или go/ast) упрощают парсинг. Но главное — Go позволяет создавать как внутренние DSL (встраиваемые в синтаксис самого Go), так и внешние DSL (с собственным синтаксисом и парсером).
Внутренние DSL на Go: используем синтаксис языка
Внутренние DSL — это библиотеки, которые выглядят как отдельный язык, но используют синтаксис Go. Они не требуют написания парсера: вы просто пишете функции, которые «склеиваются» в цепочки вызовов.
Пример: DSL для правил скидок
Допустим, мы хотим, чтобы правила выглядели так:
discount := Rule("VIP скидка").
When(City("Москва")).
When(OrderSum > 5000).
Apply(Discount(10))
Как это реализовать на Go? Используем паттерн Builder и функции высшего порядка:
package main
import "fmt"
// Rule - структура правила
type Rule struct {
Name string
Checks []func(order map[string]interface{}) bool
Discount int
}
// When добавляет условие
func (r *Rule) When(check func(order map[string]interface{}) bool) *Rule {
r.Checks = append(r.Checks, check)
return r
}
// Apply устанавливает скидку
func (r *Rule) Apply(discount int) *Rule {
r.Discount = discount
return r
}
// Evaluate проверяет, подходит ли заказ
func (r *Rule) Evaluate(order map[string]interface{}) bool {
for _, check := range r.Checks {
if !check(order) {
return false
}
}
return true
}
// Вспомогательные функции-условия
func City(city string) func(map[string]interface{}) bool {
return func(order map[string]interface{}) bool {
return order["city"] == city
}
}
func OrderSum(order map[string]interface{}) int {
return order["sum"].(int)
}
// Перегрузка оператора > через замыкание
func GreaterThan(a func(map[string]interface{}) int, b int) func(map[string]interface{}) bool {
return func(order map[string]interface{}) bool {
return a(order) > b
}
}
func main() {
// Создаём правило
rule := Rule("VIP скидка").
When(City("Москва")).
When(GreaterThan(OrderSum, 5000)).
Apply(10)
// Проверяем на заказе
order := map[string]interface{}{
"city": "Москва",
"sum": 7000,
}
fmt.Printf("Правило '%s' применимо: %v, скидка: %d%%\n",
rule.Name, rule.Evaluate(order), rule.Discount)
}
Этот код — пример внутреннего DSL на Go. Он читается как естественный язык, но остаётся валидным Go-кодом. Пользователь (менеджер) может комбинировать условия, не зная синтаксиса Go — достаточно понимать, что When добавляет условие, а Apply — результат.
Плюсы внутренних DSL
- Не нужно писать парсер — весь синтаксис уже в Go.
- Полная интеграция с экосистемой: типы, компиляция, тесты.
- Меньше ошибок — компилятор Go проверяет типы.
Минусы
- Сложнее читать для не-программистов (скобки, точки с запятой).
- Ограничен синтаксисом Go — нельзя добавить свои ключевые слова.
Внешние DSL на Go: пишем свой парсер
Если внутренний DSL не даёт нужной гибкости, создаём внешний — с собственным синтаксисом. Например, правила скидок могут выглядеть так:
rule "VIP скидка":
if city == "Москва" and sum > 5000:
discount = 10
Для этого нужен парсер. В Go есть отличные инструменты:
text/scanner— для лексического анализа (разбивка на токены).go/parser— если ваш DSL похож на Go.- Сторонние библиотеки: PEG (parsing expression grammar) или yacc.
Пример: простой парсер для DSL правил
Создадим минимальный интерпретатор. Пусть наш DSL поддерживает:
- Объявление правила:
rule "название": - Условия:
city == "Москва",sum > 5000 - Применение:
discount = 10
Реализуем лексический анализатор (токени