Building a microkernel in Golang

Why use microkernel? Ease of modification, high degree of component isolation and ease of scalability are just a few of the benefits.

Let's start with the basics: project structure

First, let's create the basic structure of our project. In Go, everything is simpler than it seems. I suggest the following organization:

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

main.go — application entry point.

kernel/ — microkernel kernel package.

modules/ — directory for all modules (for example, logger, authentication, etc.).

interfaces/ — definition of interfaces that modules must implement.

Defining Interfaces

The first step is to determine how the modules will interact with the kernel. To do this, we will create an interface in interfaces/module.go:

package interfaces

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

This interface ensures that each module can initialize with the kernel, start and stop.

Now let's define the kernel interface in the same file:

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

There is now a basic agreement on how the kernel and modules will communicate.

Let's implement the kernel

Let's move on to the core. IN kernel/kernel.go let's create the kernel structure and implement the interface 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
}

Creating modules

Let's now create a couple of modules to understand how this all works. Let's start with a simple logger.

Logger

IN 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
}

Authentication

IN 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
}

Putting it all together

Now that we have a kernel and a couple of modules, let's combine them into 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")
}

Let's expand the system

Now let's make the system a little cooler. Let modules be able to subscribe to events and react to them. This will require a subscription and notification mechanism.

Updating the Kernel interface

IN interfaces/module.go let's add a method for processing events:

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

Updating the kernel

IN kernel/kernel.go add subscriber support:

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: Stores a list of modules subscribed to each event.

Subscribe: Allows the module to subscribe to the event.

Broadcast: Distributes an event to all subscribers, executing their handlers asynchronously.

Updating modules

Modules can now process events. Let's update LoggerModuleso that it logs events:

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

And module AuthModuleso that it generates an event on successful authentication:

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
}

Don't forget to update the imports and add the necessary packages, for example, time.

Launch and test

After all the changes, let's launch our application:

go run main.go

Expected output:

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

The modules are initialized and started. AuthModule after 2 seconds generates an event UserLoggedIn. LoggerModule receives and processes the event, logging it.

All modules stop correctly.

That's it. We created a simple but flexible microkernel system in Golang, added modules that interact with each other through the kernel, and demonstrated how easy it is to expand functionality.

If you have questions, write in the comments!


Learn more relevant application architecture skills with hands-on online courses from industry experts. In the catalog you can see a list of all programs, and in the calendar — sign up for open lessons.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *