Skip to content

Введение в Processor ​

Это руководство поможет понять, когда и как использовать Processor и какие преимущества он даёт по сравнению с функциями уровня пакета.

Функции пакета vs Processor ​

CyberGo JSON предлагает два стиля API:

АспектФункции уровня пакетаProcessor
Типичный вызовjson.GetString(data, "name")p.GetString(data, "name")
Способ созданияНе требуется, вызывайте напрямуюp, err := json.New()
КонфигурацияПередача cfg ...Config при каждом вызовеЗадаётся один раз при создании, затем переиспользуется
КэшОбщий глобальный кэшСобственный кэш, управляемый и очищаемый
Управление ресурсамиАвтоматическое (глобальный процессор)Ручное Close()
Система хуковНе поддерживаетсяПоддерживается AddHook
Предварительный разборНе поддерживаетсяПоддерживается PreParse + GetFromParsed
Предкомпиляция путейНе поддерживаетсяПоддерживается CompilePath + GetCompiled
Сценарии примененияПростые операции, скрипты, редкие вызовыЧастые операции, пользовательская конфигурация, серверная сторона

Быстрый выбор

  • Функции пакета: периодическая работа с JSON, отсутствие желания управлять жизненным циклом, быстрые скрипты
  • Processor: нужна пользовательская конфигурация, частые запросы к одним и тем же данным, нужны хуки/аудит

Когда использовать Processor ​

Сценарий 1: пользовательская конфигурация ​

Функции уровня пакета используют конфигурацию по умолчанию. Если нужен режим безопасности, пользовательский кодировщик или хуки, используйте Processor:

go
// Функция пакета — всегда конфигурация по умолчанию
val := json.GetString(data, "name")

// Processor — конфигурация настраивается
cfg := json.SecurityConfig() // режим безопасности
p, err := json.New(cfg)
if err != nil {
	panic(err)
}
defer p.Close()

// Все последующие операции используют безопасную конфигурацию
val, err := p.Get(data, "name")

Сценарий 2: частые запросы к одним и тем же данным (оптимизация PreParse) ​

При многократных запросах к одному JSON PreParse выполняет разбор один раз, а последующие запросы переиспользуют результат:

go
p, err := json.New()
if err != nil {
	panic(err)
}
defer p.Close()

// Однократный разбор
parsed, err := p.PreParse(largeJSON)
if err != nil {
	panic(err)
}
defer parsed.Release() // возврат в пул объектов после использования

// Многократные запросы — переиспользование результата разбора, без повторного парсинга
name, _ := p.GetFromParsed(parsed, "user.name")
email, _ := p.GetFromParsed(parsed, "user.email")
tags, _ := p.GetFromParsed(parsed, "tags")

// Можно получить и сам результат разбора (map[string]any / []any)
data := parsed.Data()
_ = data

// Изменение тоже может опираться на предразобранный результат: SetFromParsed возвращает новый ParsedJSON, исходный не меняется
modified, err := p.SetFromParsed(parsed, "user.age", 31)
if err != nil {
	panic(err)
}
newAge, _ := p.GetFromParsed(modified, "user.age")

Сравнение производительности

  • Функция пакета GetString: каждый вызов разбирает JSON (кэш есть, но доля попаданий зависит от сценария)
  • PreParse + GetFromParsed: разбор один раз, N запросов выполняют только навигацию, ноль повторных разборов

Сценарий 3: частые запросы по одному пути (оптимизация CompilePath) ​

PreParse оптимизирует случай «один JSON запрашивается много раз»; если же «один и тот же путь многократно выполняется на большом количестве разных JSON», используйте предкомпиляцию пути CompilePath — разбор и проверка пути выполняются один раз, после чего каждый запрос сразу переходит к навигации:

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	p, err := json.New()
	if err != nil {
		panic(err)
	}
	defer p.Close()

	// Путь компилируется один раз (разбор + проверка)
	compiled, err := p.CompilePath("user.name")
	if err != nil {
		panic(err)
	}
	defer compiled.Release() // возврат в пул объектов

	// Повторяющиеся запросы в горячем пути: без разбора пути, только навигация
	for _, data := range []string{
		`{"user":{"name":"Alice"}}`,
		`{"user":{"name":"Bob"}}`,
	} {
		val, err := p.GetCompiled(data, compiled)
		if err != nil {
			panic(err)
		}
		fmt.Println(val)
	}
	// Вывод:
	// Alice
	// Bob
}

Разделение труда двух оптимизаций

ОптимизацияУстраняемые накладные расходыСценарий применения
PreParse + GetFromParsedПовторный разбор JSON-документаОдин JSON, много разных путей
CompilePath + GetCompiledПовторный разбор выражения путиОдин путь на множестве JSON (горячий путь)

Это независимые направления оптимизации — выбирайте по узкому месту. Обратите внимание: у GetCompiled пока есть только вариант для запросов, Set/Delete предкомпилированные пути пока не поддерживают; изменение на стороне предразбора доступно через SetFromParsed.

Сценарий 4: хуки и аудит ​

Когда нужны журналирование, мониторинг производительности или проверка входных данных, Processor поддерживает систему хуков:

go
p, err := json.New()
if err != nil {
	panic(err)
}
defer p.Close()

// Добавить хук логирования
p.AddHook(json.LoggingHook(slog.Default()))
// Добавить хук замеров времени
p.AddHook(json.TimingHook(&metricsRecorder))

// Хуки срабатывают автоматически для всех операций
result, err := p.Set(data, "user.name", "Alice")

Подробнее см. Система хуков Hook.

Сценарий 5: разделение Processor между горутинами ​

Processor потокобезопасен — правильный паттерн: создать один раз, разделять всей группой, закрыть один раз в конце, а не создавать по процессору на каждый запрос (последнее лишь добавляет накладные расходы создания и умножает стоимость управления ресурсами):

go
package main

import (
	"fmt"
	"sync"

	"github.com/cybergodev/json"
)

func main() {
	p, err := json.New()
	if err != nil {
		panic(err)
	}
	defer p.Close() // выполнится только после завершения всех горутин

	data := `{"user":{"name":"Alice","age":30}}`

	var wg sync.WaitGroup
	for i := 1; i <= 8; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			name := p.GetString(data, "user.name")
			age := p.GetInt(data, "user.age")
			fmt.Printf("горутина %d: %s (%d)\n", i, name, age)
		}(i)
	}
	wg.Wait()

	stats := p.GetStats()
	fmt.Println("Всего операций:", stats.OperationCount)
}

// Вывод (порядок горутин не определён):
// горутина 5: Alice (30)
// горутина 2: Alice (30)
// ...
// Всего операций: 16

MaxConcurrency — мягкий предел

По умолчанию MaxConcurrency = 50: когда операций в полёте больше этого значения, новые операции немедленно завершаются ошибкой ErrConcurrencyLimit (без постановки в очередь). В высоконагруженных сервисах увеличьте значение по необходимости или организуйте ограничение частоты и повторные попытки на стороне вызывающего.

Сценарий 6: единая глобальная конфигурация ​

За функциями уровня пакета стоит глобальный процессор. Когда хочется, чтобы всё приложение — включая старый код, параметры которого уже не переделать, — работало с одной конфигурацией, замените его одним вызовом SetGlobalProcessor: все пакетные вызовы json.Get/json.Marshal и т.д. немедленно переключатся. Полный пример и предостережения — в разделе Глобальный процессор ниже.

Управление жизненным циклом ​

Processor удерживает ресурсы (кэш, горутины), после использования его обязательно нужно закрыть:

go
p, err := json.New()
if err != nil {
	panic(err)
}
defer p.Close() // гарантия освобождения ресурсов

// Использование Processor...
result, err := p.GetString(data, "name")

Последствия забытого Close

  • Память кэша не освобождается
  • Утечка фоновых горутин
  • При высокой конкурентности возможно исчерпание ресурсов

Проверка состояния ​

go
if p.IsClosed() {
	// Processor закрыт и больше не может использоваться
}

IsClosed возвращает true в обоих состояниях: полностью закрыт либо закрывается (период ожидания дренирования) или закрытие превысило таймаут. В обоих состояниях новые операции отклоняются с ошибкой, поэтому этой проверки достаточно как единственного критерия «можно ли ещё использовать».

Мониторинг и диагностика ​

Processor имеет встроенную статистику работы и проверку здоровья — удобно подключать к мониторингу сервисов:

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	p, err := json.New()
	if err != nil {
		panic(err)
	}
	defer p.Close()

	_, _ = p.Get(`{"user":{"name":"Alice"}}`, "user.name")

	// Статистика работы: число операций, ошибок, доля попаданий в кэш и память
	stats := p.GetStats()
	fmt.Printf("операции=%d ошибки=%d попадания=%.2f записи кэша=%d\n",
		stats.OperationCount, stats.ErrorCount, stats.HitRatio, stats.CacheSize)

	// Проверка здоровья: результаты отдельных проверок кэша, памяти и др.
	health := p.GetHealthStatus()
	fmt.Println("здоров:", health.Healthy)
	for name, check := range health.Checks {
		fmt.Printf("  %s: %s\n", name, check.Message)
	}

	// Чтение текущей конфигурации (возвращается копия, изменения не затрагивают Processor)
	cfg := p.GetConfig()
	fmt.Println("кэш включён:", cfg.EnableCache)
}

Версия на уровне пакета

У глобального процессора есть и пакетные точки мониторинга: json.GetStats() и json.GetHealthStatus() — удобны для глобальной диагностики из кода, не держащего ссылку на Processor. Статистика кэша и полное использование ClearCache/WarmupCache описаны в Стратегиях кэширования и предпарсинга.

Глобальный процессор ​

Функции уровня пакета (Get, Set, Marshal и др.) внутри используют глобальный процессор. Его можно заменить:

go
// Создание процессора с пользовательской конфигурацией
cfg := json.SecurityConfig()
p, err := json.New(cfg)
if err != nil {
	panic(err)
}

// Назначение глобальным процессором
json.SetGlobalProcessor(p)

// Теперь все функции уровня пакета используют безопасную конфигурацию
val := json.GetString(data, "name")

// Очистка при завершении приложения
defer json.ShutdownGlobalProcessor()

Детали поведения:

  • SetGlobalProcessor потокобезопасен, передача nil — no-op; при замене старый процессор закрывается автоматически
  • ShutdownGlobalProcessor — полная очистка при выходе: помимо закрытия глобального процессора она закрывает процессоры «с кэшированием по конфигурации» и очищает глобальные кэши путей/кодирования; после этого пакетные функции автоматически создают новый процессор по умолчанию
  • Пакетные функции с cfg (например, json.Get(data, path, json.SecurityConfig())) идут через процессоры с кэшированием по конфигурации, минуя глобальный процессор — два механизма работают параллельно и не влияют друг на друга

Сценарии применения

  • Единая глобальная политика безопасности
  • Глобальное действие пользовательского кодировщика
  • Замена конфигурации по умолчанию без передачи Config повсюду

Дерево решений ​

Нужно работать с JSON?
├── Редкое использование, скрипты и утилиты
│   └── → функции пакета json.GetString / json.Set / json.Marshal
├── Редкое использование, но нужна конфигурация безопасности/кодирования
│   └── → функции пакета с хвостовым cfg: json.Get(data, path, json.SecurityConfig())
├── Частое использование или нужны возможности процессора (хуки и др.)
│   └── → Processor json.New(cfg)
├── Многократные запросы к одному JSON
│   └── → Processor + PreParse
├── Один путь на большом количестве JSON (горячий путь)
│   └── → Processor + CompilePath
├── Параллельная обработка несколькими горутинами
│   └── → один общий Processor (потокобезопасен), не создавайте по одному на запрос
├── Нужны аудит/мониторинг/логирование
│   └── → Processor + AddHook
├── Нужны метрики времени выполнения/проверка здоровья
│   └── → GetStats / GetHealthStatus (методы Processor и функции пакета)
└── Единая глобальная конфигурация
    └── → SetGlobalProcessor

Что дальше ​