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 LoggerModule
so 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 AuthModule
so 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.