DSL на Go: как создать предметно-ориентированный язык для реальных задач

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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

Реализуем лексический анализатор (токени

Блоги

Рекомендаций по книгам