Как реализовать расширяемость процессора для добавления новых типов событий без изменения кода
Что проверяют
Понимаете ли вы принцип открытости/закрытости в идиомах Go: интерфейсы, реестр обработчиков, регистрация без правки ядра. Следом обычно спрашивают, как десериализовать событие заранее неизвестного типа и что делать с типом, для которого обработчика нет.
Как отвечать
Разделите событие и обработчик. Событие это данные с типом, обработчик это поведение:
- конверт:
type Envelope struct { Type string; Payload json.RawMessage }, тип читается первым, тело разбирается позже; - интерфейс:
type Handler interface { Handle(ctx context.Context, payload json.RawMessage) error }, узкий, из одного метода; - реестр:
map[string]Handlerс методомRegister(eventType string, h Handler). Процессор ищет обработчик поEnvelope.Typeи ничего не знает о конкретных событиях; - обёртка на дженериках:
func On[T any](r *Registry, typ string, fn func(context.Context, T) error)сама делаетjson.UnmarshalвT, и автор обработчика работает со структурой, а не с байтами; - регистрация: явно в
mainили там, где собираются зависимости. Вариант сinit()и пустым импортом пакета встречается, как у драйверовdatabase/sql, но прячет зависимости.
Честно оговорите, что «без изменения кода» значит без изменения ядра: новый тип это новый файл с обработчиком и одна строка регистрации. Подгружаемые модули в Go возможны, но пакет plugin работает не на всех платформах и требует совпадения версии компилятора и зависимостей. На практике берут отдельные процессы через gRPC (hashicorp/go-plugin) или WebAssembly.
Где спотыкаются
Пишут switch e := event.(type) внутри процессора. Каждый новый тип требует правки этого switch, то есть ровно того, чего просили избежать.
Регистрируют обработчики из разных горутин во время работы без мьютекса. Конкурентная запись в map ловится рантаймом как fatal error, её не перехватить через recover. Либо регистрация до старта, либо sync.RWMutex.
Падают на неизвестном типе. Правильнее залогировать, отправить событие в dead letter и продолжить, иначе один новый тип от соседнего сервиса останавливает весь поток.