> Как реализовать расширяемость процессора для добавления новых типов событий без изменения кода (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: Wildberries
Стек: Go
> Пример ответа
Короткий ответ
Расширяемость процессора событий в Go достигается через комбинацию интерфейсов, реестра обработчиков и паттерна "фабрика". Основная идея - отделить механизм диспетчеризации от конкретной логики обработки. Новый тип события добавляется регистрацией нового обработчика в реестре, без модификации существующего кода процессора. Используйте пустые интерфейсы для данных события и type switch или map[string]Handler для маршрутизации.
Подробное объяснение
Ключевая проблема - соблюдение принципа открытости/закрытости (Open/Closed Principle). Процессор не должен знать о конкретных типах событий, только о контракте обработчика.
Архитектурно решение состоит из трёх частей:
- Контракт обработчика - интерфейс с методом Handle(ctx, event) error. Каждый тип события реализует свой обработчик.
- Реестр - map[string]Handler, где ключ - тип события (строка или типобезопасная константа). Регистрация происходит в init() или явной функцией Register.
- Диспетчер - получает событие, определяет его тип и вызывает соответствующий обработчик из реестра.
Для определения типа события используйте один из подходов:
- type switch - если события представлены конкретными типами, реализующими общий интерфейс Event.
- поле Type - если события - структуры с полем-дискриминатором (например, JSON-сообщения).
Второй подход более гибкий для распределённых систем, где события приходят по сети. Первый - типобезопасный и предпочтителен для внутреннего использования.
Реестр можно сделать глобальным (пакет registry) или внедрять через DI. Глобальный реестр проще, но усложняет тестирование. DI-подход чище, но требует передачи реестра в конструктор процессора.
Важный нюанс - обработка неизвестных типов. Процессор должен иметь fallback-обработчик или возвращать ошибку ErrUnknownEventType. Это часть контракта.
Для асинхронной обработки добавьте worker pool, но это уже отдельный уровень абстракции, не влияющий на расширяемость.
На практике
Начните с определения интерфейса Event и Handler. Реестр сделайте отдельным пакетом, чтобы избежать циклических импортов. Используйте sync.RWMutex для потокобезопасности, если регистрация может происходить в рантайме.
Для событий из внешних источников (Kafka, RabbitMQ) используйте поле Type + JSON-десериализацию в map[string]interface{} или json.RawMessage. Это позволяет добавлять новые поля без изменения процессора.
Регистрацию обработчиков делайте в init() каждого пакета, где определён обработчик. Это автоматически подключает обработчик при импорте пакета (side-effect import). Но будьте осторожны - такой подход скрывает зависимости.
Для тестирования создавайте реестр с мок-обработчиками и проверяйте диспетчеризацию.
Пример кода
GOpackage eventimport ("context""fmt""sync")type Event interface {Type() string}type Handler interface {Handle(ctx context.Context, event Event) error}type Registry struct {mu sync.RWMutexhandlers map[string]Handler}func NewRegistry() *Registry {return &Registry{handlers: make(map[string]Handler),}}func (r *Registry) Register(eventType string, h Handler) {r.mu.Lock()defer r.mu.Unlock()r.handlers[eventType] = h}func (r *Registry) Dispatch(ctx context.Context, event Event) error {r.mu.RLock()handler, ok := r.handlers[event.Type()]r.mu.RUnlock()if !ok {return fmt.Errorf("unknown event type: %s", event.Type())}return handler.Handle(ctx, event)}// Конкретное событиеtype UserCreated struct {UserID string}func (e UserCreated) Type() string { return "user.created" }// Обработчикtype UserCreatedHandler struct{}func (h UserCreatedHandler) Handle(ctx context.Context, event Event) error {e, ok := event.(UserCreated)if !ok {return fmt.Errorf("unexpected event type: %T", event)}// логика обработкиfmt.Printf("user created: %s\n", e.UserID)return nil}// Регистрация в init()func init() {registry := GetGlobalRegistry() // глобальный синглтонregistry.Register(UserCreated{}.Type(), UserCreatedHandler{})}
Как отвечать на собеседовании
Начните с формулировки проблемы: "процессор не должен знать о конкретных типах". Затем опишите три компонента: интерфейс, реестр, диспетчер. Подчеркните, что добавление нового типа - это новый файл с событием и обработчиком + регистрация, без изменения существующего кода.
Упомяните trade-off между type switch и map[string]Handler. Для senior-позиции важно показать понимание, когда какой подход применить. Например, type switch - для внутренних событий с гарантией типов, map - для внешних сообщений.
Расскажите про потокобезопасность реестра, обработку неизвестных типов, тестирование. Если спросят про альтернативы - упомяните паттерн Visitor или цепочку обязанностей, но объясните, почему реестр проще и практичнее.
Что проверяет интервьюер
- Понимание принципов SOLID, особенно открытости/закрытости.
- Умение проектировать расширяемые системы без переписывания ядра.
- Знание идиоматичного Go: интерфейсы, пустые интерфейсы, sync.RWMutex.
- Способность видеть trade-off между типобезопасностью и гибкостью.
- Понимание жизненного цикла приложения: инициализация, регистрация, диспетчеризация.
- Навыки тестирования: как проверить диспетчер без реальных обработчиков.
Типичные ошибки
- Использование глобального реестра без возможности подмены в тестах.
- Отсутствие обработки неизвестных типов - молчаливый пропуск события.
- Регистрация обработчиков в main() вместо init() - усложняет подключение новых пакетов.
- Хранение обработчиков в map без RWMutex - data race при конкурентной регистрации.
- Попытка использовать type switch на пустом интерфейсе без проверки типа - паника.
- Слишком сложная иерархия интерфейсов вместо простого контракта.
- Игнорирование контекста в сигнатуре Handle - нельзя отменить операцию.
> Похожие задачи по Go
Как расположены ключи и значения в бакетах хэш-таблицы в Go?
Есть ли параметры у мапы в Go, например возможность заранее задать размер
Как скопировать все значения большой мапы в Go
Какую проблему решает Kubernetes
> Похожие задачи по backend
Как расположены ключи и значения в бакетах хэш-таблицы в Go?
Есть ли параметры у мапы в Go, например возможность заранее задать размер
Как скопировать все значения большой мапы в Go
Какую проблему решает Kubernetes
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью