Строим микрокernel на Golang

от автора

Привет, Хабр! Сегодня рассмотрим, как построить гибкую и масштабируемую систему с использованием микрокernel архитектуры на Golang.

Перед тем как взяться за код, разберёмся, о чём вообще идёт речь. Микрокernel — это архитектурный стиль, при котором минимальное ядро системы отвечает за основные функции: управление процессами, памятью, коммуникациями и т. д., а всё остальное делегируется в виде отдельных модулей или сервисов.

Почему стоит использовать микрокernel? Простота модификации, высокая степень изоляции компонентов и легкость масштабирования — лишь малая часть преимуществ.

Начнём с основ: структура проекта

Для начала создадим базовую структуру нашего проекта. В Go всё проще, чем кажется. Предлагаю следующую организацию:

microkernel/ ├── main.go ├── kernel/ │   └── kernel.go ├── modules/ │   ├── logger/ │   │   └── logger.go │   └── auth/ │       └── auth.go └── interfaces/     └── module.go

main.go — точка входа приложения.

kernel/ — пакет ядра микрокernel.

modules/ — директория для всех модулей (например, логгер, аутентификация и т.д.).

interfaces/ — определение интерфейсов, которые должны реализовывать модули.

Определяем интерфейсы

Первым делом нужно определить, как модули будут взаимодействовать с ядром. Для этого создадим интерфейс в interfaces/module.go:

package interfaces  type Module interface {     Init(kernel Kernel) error     Start() error     Stop() error }

Этот интерфейс гарантирует, что каждый модуль сможет инициализироваться с ядром, запускаться и останавливаться.

А теперь определим интерфейс ядра в том же файле:

type Kernel interface {     RegisterModule(name string, module Module) error     GetModule(name string) (Module, error)     Broadcast(event string, data interface{}) error }

Теперь есть базовая договорённость о том, как ядро и модули будут общаться.

Реализуем ядро

Перейдём к ядру. В kernel/kernel.go создадим структуру ядра и реализуем интерфейс Kernel:

package kernel  import (     "errors"     "fmt"     "sync"      "../interfaces" )  type Microkernel struct {     modules map[string]interfaces.Module     mu      sync.RWMutex }  func New() *Microkernel {     return &Microkernel{         modules: make(map[string]interfaces.Module),     } }  func (k *Microkernel) RegisterModule(name string, module interfaces.Module) error {     k.mu.Lock()     defer k.mu.Unlock()      if _, exists := k.modules[name]; exists {         return fmt.Errorf("module %s already registered", name)     }      k.modules[name] = module     return nil }  func (k *Microkernel) GetModule(name string) (interfaces.Module, error) {     k.mu.RLock()     defer k.mu.RUnlock()      module, exists := k.modules[name]     if !exists {         return nil, fmt.Errorf("module %s not found", name)     }     return module, nil }  func (k *Microkernel) Broadcast(event string, data interface{}) error {     // Простая реализация: просто выводим событие     fmt.Printf("Broadcasting event: %s with data: %v\n", event, data)     return nil }

Создаём модули

Давайте теперь создадим пару модулей, чтобы понять, как всё это работает. Начнём с простого логгера.

Логгер

В modules/logger/logger.go:

package logger  import (     "fmt"     "../interfaces"     "../kernel" )  type LoggerModule struct {     kernel interfaces.Kernel }  func NewLogger() *LoggerModule {     return &LoggerModule{} }  func (l *LoggerModule) Init(k interfaces.Kernel) error {     l.kernel = k     fmt.Println("Logger module initialized")     return nil }  func (l *LoggerModule) Start() error {     fmt.Println("Logger module started")     // Можно подписаться на события ядра     return nil }  func (l *LoggerModule) Stop() error {     fmt.Println("Logger module stopped")     return nil }

Аутентификация

В modules/auth/auth.go:

package auth  import (     "fmt"     "../interfaces"     "../kernel" )  type AuthModule struct {     kernel interfaces.Kernel }  func NewAuth() *AuthModule {     return &AuthModule{} }  func (a *AuthModule) Init(k interfaces.Kernel) error {     a.kernel = k     fmt.Println("Auth module initialized")     return nil }  func (a *AuthModule) Start() error {     fmt.Println("Auth module started")     // Например, инициализируем базу данных пользователей     return nil }  func (a *AuthModule) Stop() error {     fmt.Println("Auth module stopped")     return nil }

Собираем всё вместе

Теперь, когда есть ядро и пару модулей, давайте соединим их в main.go:

package main  import (     "fmt"     "log"      "./kernel"     "./interfaces"     "./modules/auth"     "./modules/logger" )  func main() {     // Создаём ядро     k := kernel.New()      // Создаём модули     loggerModule := logger.NewLogger()     authModule := auth.NewAuth()      // Регистрируем модули     if err := k.RegisterModule("logger", loggerModule); err != nil {         log.Fatalf("Error registering logger module: %v", err)     }      if err := k.RegisterModule("auth", authModule); err != nil {         log.Fatalf("Error registering auth module: %v", err)     }      // Инициализируем модули     if err := loggerModule.Init(k); err != nil {         log.Fatalf("Error initializing logger module: %v", err)     }      if err := authModule.Init(k); err != nil {         log.Fatalf("Error initializing auth module: %v", err)     }      // Запускаем модули     if err := loggerModule.Start(); err != nil {         log.Fatalf("Error starting logger module: %v", err)     }      if err := authModule.Start(); err != nil {         log.Fatalf("Error starting auth module: %v", err)     }      // Пример использования ядра     k.Broadcast("UserLoggedIn", map[string]string{         "username": "john_doe",     })      // Останавливаем модули перед завершением     if err := authModule.Stop(); err != nil {         log.Fatalf("Error stopping auth module: %v", err)     }      if err := loggerModule.Stop(); err != nil {         log.Fatalf("Error stopping logger module: %v", err)     }      fmt.Println("Microkernel system shutdown gracefully") }

Расширим систему

Теперь сделаем систему чуть более круче. Пусть модули могут подписываться на события и реагировать на них. Для этого понадобится механизм подписки и уведомления.

Обновляем интерфейс Kernel

В interfaces/module.go добавим метод для обработки событий:

type Module interface {     Init(kernel Kernel) error     Start() error     Stop() error     HandleEvent(event string, data interface{}) error }

Обновляем ядро

В kernel/kernel.go добавим поддержку подписчиков:

type Microkernel struct {     modules     map[string]interfaces.Module     subscribers map[string][]interfaces.Module     mu          sync.RWMutex }  func New() *Microkernel {     return &Microkernel{         modules:     make(map[string]interfaces.Module),         subscribers: make(map[string][]interfaces.Module),     } }  func (k *Microkernel) Subscribe(event string, module interfaces.Module) {     k.mu.Lock()     defer k.mu.Unlock()     k.subscribers[event] = append(k.subscribers[event], module) }  func (k *Microkernel) Broadcast(event string, data interface{}) error {     k.mu.RLock()     defer k.mu.RUnlock()      subscribers, exists := k.subscribers[event]     if !exists {         fmt.Printf("No subscribers for event: %s\n", event)         return nil     }      for _, module := range subscribers {         go func(m interfaces.Module) {             if err := m.HandleEvent(event, data); err != nil {                 fmt.Printf("Error handling event %s in module: %v\n", event, err)             }         }(module)     }      return nil }

subscribers: Хранит список модулей, подписанных на каждое событие.

Subscribe: Позволяет модулю подписаться на событие.

Broadcast: Рассылает событие всем подписчикам, выполняя их обработчики асинхронно.

Обновляем модули

Теперь модули могут обрабатывать события. Обновим LoggerModule, чтобы он логировал события:

func (l *LoggerModule) HandleEvent(event string, data interface{}) error {     fmt.Printf("[Logger] Event received: %s with data: %v\n", event, data)     return nil }

И модуль AuthModule, чтобы он генерировал событие при успешной аутентификации:

func (a *AuthModule) Start() error {     fmt.Println("Auth module started")     // Имитация аутентификации пользователя     go func() {         // Пауза для имитации процесса         time.Sleep(2 * time.Second)         a.kernel.Broadcast("UserLoggedIn", map[string]string{             "username": "john_doe",         })     }()     return nil }

Не забываем обновить импорты и добавить необходимые пакеты, например, time.

Запускаем и тестируем

После всех изменений, запустим наше приложение:

go run main.go

Ожидаемый вывод:

Logger module initialized Auth module initialized Logger module started Auth module started Broadcasting event: UserLoggedIn with data: map[username:john_doe] [Logger] Event received: UserLoggedIn with data: map[username:john_doe] Auth module stopped Logger module stopped Microkernel system shutdown gracefully

Модули инициализируются и запускаются. AuthModule через 2 секунды генерирует событие UserLoggedIn. LoggerModule получает и обрабатывает событие, логируя его.

Все модули корректно останавливаются.

Вот и все. Создали простую, но гибкую микрокernel систему на Golang, добавили модули, которые взаимодействуют между собой через ядро, и продемонстрировали, как легко расширять функционал.

Если у вас есть вопросы, пишите в комментариях!


Больше актуальных навыков по архитектуре приложений вы можете получить в рамках практических онлайн-курсов от экспертов отрасли. В каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.


ссылка на оригинал статьи https://habr.com/ru/articles/854120/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *