Modules
GOE's module system is the foundation of its extensible architecture. Modules allow you to organize functionality into cohesive units with managed lifecycles. This guide covers both using built-in modules and creating custom modules.
Overview
GOE modules provide:
- Lifecycle Management: Automatic startup and shutdown handling
- Dependency Injection: Seamless integration with Fx
- Modular Architecture: Enable only what you need
- Extensibility: Easy to create custom modules
Built-in Modules
GOE comes with several built-in modules that you can enable as needed:
Core Modules
goe.New(goe.Options{
WithHTTP: true, // HTTP server with GoFiber
WithDB: true, // Database integration with GORM
WithCache: true, // Caching with multiple backends
WithObservability: true, // Metrics and tracing
})
Module Dependencies
Some modules have dependencies:
- HTTP Module: Depends on Config and Logger
- Database Module: Depends on Config and Logger
- Cache Module: Depends on Config and Logger
- Observability Module: Depends on Config and Logger
GOE automatically resolves these dependencies.
Module Interface
All GOE modules implement the contract.Module
interface:
package contract
import "context"
type Module interface {
Name() string // Unique module identifier
OnStart(ctx context.Context) error // Initialization logic
OnStop(ctx context.Context) error // Cleanup logic
}
Creating Custom Modules
Basic Module Structure
package mymodule
import (
"context"
"go.oease.dev/goe/v2/contract"
)
type Module struct {
logger contract.Logger
config contract.Config
// Add your module's dependencies
}
func NewModule(logger contract.Logger, config contract.Config) *Module {
return &Module{
logger: logger,
config: config,
}
}
func (m *Module) Name() string {
return "my-module"
}
func (m *Module) OnStart(ctx context.Context) error {
m.logger.Info("Starting my module")
// Initialize your module here
// Connect to external services, start background tasks, etc.
return nil
}
func (m *Module) OnStop(ctx context.Context) error {
m.logger.Info("Stopping my module")
// Clean up resources here
// Close connections, stop background tasks, etc.
return nil
}
Service Provider Module
A module that provides a service to other parts of the application:
package emailmodule
import (
"context"
"go.oease.dev/goe/v2/contract"
)
type EmailService interface {
SendEmail(to, subject, body string) error
}
type emailService struct {
logger contract.Logger
config contract.Config
}
func (s *emailService) SendEmail(to, subject, body string) error {
s.logger.Info("Sending email", "to", to, "subject", subject)
// Email sending logic here
return nil
}
type Module struct {
service EmailService
logger contract.Logger
}
func NewModule(logger contract.Logger, config contract.Config) *Module {
return &Module{
service: &emailService{logger: logger, config: config},
logger: logger,
}
}
func (m *Module) Name() string {
return "email-module"
}
func (m *Module) OnStart(ctx context.Context) error {
m.logger.Info("Email module started")
return nil
}
func (m *Module) OnStop(ctx context.Context) error {
m.logger.Info("Email module stopped")
return nil
}
// Provide the service to other components
func (m *Module) EmailService() EmailService {
return m.service
}
Background Task Module
A module that runs background tasks:
package taskmodule
import (
"context"
"time"
"go.oease.dev/goe/v2/contract"
)
type Module struct {
logger contract.Logger
config contract.Config
done chan struct{}
}
func NewModule(logger contract.Logger, config contract.Config) *Module {
return &Module{
logger: logger,
config: config,
done: make(chan struct{}),
}
}
func (m *Module) Name() string {
return "task-module"
}
func (m *Module) OnStart(ctx context.Context) error {
m.logger.Info("Starting background task module")
// Start background goroutine
go m.backgroundTask()
return nil
}
func (m *Module) OnStop(ctx context.Context) error {
m.logger.Info("Stopping background task module")
// Signal background task to stop
close(m.done)
return nil
}
func (m *Module) backgroundTask() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.logger.Info("Running background task")
// Perform background work
case <-m.done:
m.logger.Info("Background task stopped")
return
}
}
}
Registering Custom Modules
Method 1: Direct Module Registration
package main
import (
"go.oease.dev/goe/v2"
"yourapp/internal/modules/email"
"yourapp/internal/modules/task"
)
func main() {
goe.New(goe.Options{
WithHTTP: true,
Modules: []contract.Module{
email.NewModule,
task.NewModule,
},
})
goe.Run()
}
Method 2: Using Fx Providers
package main
import (
"go.oease.dev/goe/v2"
"yourapp/internal/modules/email"
)
func main() {
goe.New(goe.Options{
WithHTTP: true,
Providers: []any{
email.NewModule,
},
Invokers: []any{
func(emailModule *email.Module) {
// Use the email module
service := emailModule.EmailService()
// Register routes that use the email service
},
},
})
goe.Run()
}
Module Configuration
Environment-Based Configuration
package mymodule
type Config struct {
Enabled bool
APIKey string
Timeout time.Duration
Workers int
}
func NewConfig(config contract.Config) *Config {
return &Config{
Enabled: config.GetBoolWithDefault("MYMODULE_ENABLED", true),
APIKey: config.GetString("MYMODULE_API_KEY"),
Timeout: config.GetDurationWithDefault("MYMODULE_TIMEOUT", 30*time.Second),
Workers: config.GetIntWithDefault("MYMODULE_WORKERS", 5),
}
}
func NewModule(logger contract.Logger, config contract.Config) *Module {
moduleConfig := NewConfig(config)
return &Module{
logger: logger,
config: moduleConfig,
}
}
Conditional Module Loading
func main() {
config := goe.Config()
options := goe.Options{
WithHTTP: true,
}
// Conditionally enable modules
if config.GetBool("EMAIL_ENABLED") {
options.Providers = append(options.Providers, email.NewModule)
}
if config.GetBool("TASK_ENABLED") {
options.Providers = append(options.Providers, task.NewModule)
}
goe.New(options)
goe.Run()
}
Module Communication
Service Injection
// Email module provides EmailService
func (m *EmailModule) EmailService() EmailService {
return m.service
}
// User module uses EmailService
type UserModule struct {
emailService EmailService
logger contract.Logger
}
func NewUserModule(emailService EmailService, logger contract.Logger) *UserModule {
return &UserModule{
emailService: emailService,
logger: logger,
}
}
func (m *UserModule) CreateUser(name, email string) error {
// Create user logic...
// Send welcome email
return m.emailService.SendEmail(email, "Welcome!", "Welcome to our app!")
}
Event-Driven Communication
type EventBus interface {
Publish(event string, data interface{})
Subscribe(event string, handler func(data interface{}))
}
type UserCreatedEvent struct {
UserID int
Email string
}
// User module publishes events
func (m *UserModule) CreateUser(name, email string) error {
user := &User{Name: name, Email: email}
// Save user...
// Publish event
m.eventBus.Publish("user.created", UserCreatedEvent{
UserID: user.ID,
Email: user.Email,
})
return nil
}
// Email module subscribes to events
func (m *EmailModule) OnStart(ctx context.Context) error {
m.eventBus.Subscribe("user.created", func(data interface{}) {
event := data.(UserCreatedEvent)
m.service.SendEmail(event.Email, "Welcome!", "Welcome to our app!")
})
return nil
}
Testing Custom Modules
Unit Testing
package mymodule
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockLogger struct {
mock.Mock
}
func (m *MockLogger) Info(msg string, keysAndValues ...interface{}) {
m.Called(msg, keysAndValues)
}
type MockConfig struct {
mock.Mock
}
func (m *MockConfig) GetString(key string) string {
args := m.Called(key)
return args.String(0)
}
func TestModule_OnStart(t *testing.T) {
mockLogger := &MockLogger{}
mockConfig := &MockConfig{}
mockLogger.On("Info", "Starting my module", mock.Anything)
module := NewModule(mockLogger, mockConfig)
err := module.OnStart(context.Background())
assert.NoError(t, err)
mockLogger.AssertExpectations(t)
}
Integration Testing
func TestModule_Integration(t *testing.T) {
app := goe.New(goe.Options{
Modules: []contract.Module{
NewModule,
},
})
// Test module integration
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := app.Start(ctx)
assert.NoError(t, err)
// Test module functionality
err = app.Stop(ctx)
assert.NoError(t, err)
}
Best Practices
1. Single Responsibility
Each module should have a single, well-defined responsibility:
// Good - focused on email functionality
type EmailModule struct { ... }
// Bad - too many responsibilities
type EmailNotificationTaskModule struct { ... }
2. Graceful Shutdown
Always implement proper cleanup in OnStop
:
func (m *Module) OnStop(ctx context.Context) error {
// Close connections
if m.connection != nil {
m.connection.Close()
}
// Stop background tasks
if m.done != nil {
close(m.done)
}
// Cancel contexts
if m.cancel != nil {
m.cancel()
}
return nil
}
3. Error Handling
Handle errors appropriately in lifecycle methods:
func (m *Module) OnStart(ctx context.Context) error {
conn, err := m.connectToService()
if err != nil {
return fmt.Errorf("failed to connect to service: %w", err)
}
m.connection = conn
return nil
}
4. Configuration Validation
Validate configuration during module initialization:
func NewModule(logger contract.Logger, config contract.Config) *Module {
apiKey := config.GetString("MYMODULE_API_KEY")
if apiKey == "" {
logger.Fatal("MYMODULE_API_KEY is required")
}
return &Module{
logger: logger,
apiKey: apiKey,
}
}
Module Examples
Database Connection Pool Module
type DatabasePoolModule struct {
logger contract.Logger
config contract.Config
pool *sql.DB
}
func (m *DatabasePoolModule) OnStart(ctx context.Context) error {
dsn := m.config.GetString("DATABASE_URL")
pool, err := sql.Open("postgres", dsn)
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
if err := pool.Ping(); err != nil {
return fmt.Errorf("failed to ping database: %w", err)
}
m.pool = pool
m.logger.Info("Database pool initialized")
return nil
}
func (m *DatabasePoolModule) OnStop(ctx context.Context) error {
if m.pool != nil {
m.pool.Close()
}
return nil
}
func (m *DatabasePoolModule) DB() *sql.DB {
return m.pool
}
Redis Client Module
type RedisModule struct {
logger contract.Logger
config contract.Config
client *redis.Client
}
func (m *RedisModule) OnStart(ctx context.Context) error {
client := redis.NewClient(&redis.Options{
Addr: m.config.GetString("REDIS_ADDR"),
Password: m.config.GetString("REDIS_PASSWORD"),
DB: m.config.GetInt("REDIS_DB"),
})
if err := client.Ping(ctx).Err(); err != nil {
return fmt.Errorf("failed to connect to Redis: %w", err)
}
m.client = client
m.logger.Info("Redis client initialized")
return nil
}
func (m *RedisModule) Client() *redis.Client {
return m.client
}
Next Steps
- Dependency Injection - Learn more about Fx patterns
- Testing - Test your custom modules
- Best Practices - Module development best practices