Configuration
GOE provides a flexible configuration system that loads settings from environment variables and .env
files. This makes it easy to manage different configurations for development, staging, and production environments.
Configuration Sources
GOE loads configuration from multiple sources in the following priority order:
- System environment variables (highest priority)
.env.{GOE_ENV}
file (e.g.,.env.production
).env.local
file.env
file (lowest priority)
Environment Files
Basic .env File
Create a .env
file in your project root:
# Application
APP_NAME=My GOE App
APP_VERSION=1.0.0
APP_ENV=development
# HTTP Server
HTTP_HOST=0.0.0.0
HTTP_PORT=8080
# Database
DB_DRIVER=postgres
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=secret
DB_SSL_MODE=disable
# Caching
CACHE_DRIVER=memory
CACHE_PREFIX=myapp
CACHE_TTL=30m
# Logging
LOG_LEVEL=info
LOG_FORMAT=console
Environment-Specific Files
Create different files for different environments:
# .env.development
APP_ENV=development
LOG_LEVEL=debug
LOG_FORMAT=console
DB_HOST=localhost
# .env.production
APP_ENV=production
LOG_LEVEL=warn
LOG_FORMAT=json
DB_HOST=prod-db.example.com
# .env.test
APP_ENV=test
LOG_LEVEL=error
DB_NAME=myapp_test
Set the environment:
export GOE_ENV=production
Accessing Configuration
Using Global Accessor
import "go.oease.dev/goe/v2"
func someFunction() {
config := goe.Config()
// Get string values
appName := config.GetString("APP_NAME")
dbHost := config.GetString("DB_HOST")
// Get integer values
httpPort := config.GetInt("HTTP_PORT")
dbPort := config.GetInt("DB_PORT")
// Get boolean values
debugMode := config.GetBool("DEBUG_MODE")
// Get values with defaults
timeout := config.GetIntWithDefault("TIMEOUT", 30)
env := config.GetStringWithDefault("APP_ENV", "development")
}
Using Dependency Injection
import "go.oease.dev/goe/v2/contract"
type DatabaseService struct {
config contract.Config
}
func NewDatabaseService(config contract.Config) *DatabaseService {
return &DatabaseService{config: config}
}
func (s *DatabaseService) Connect() error {
host := s.config.GetString("DB_HOST")
port := s.config.GetInt("DB_PORT")
database := s.config.GetString("DB_NAME")
// Use configuration to connect to database
connectionString := fmt.Sprintf("host=%s port=%d dbname=%s", host, port, database)
return nil
}
Configuration Overrides
GOE provides a powerful feature to override configuration values programmatically through the Options
struct. This allows you to set configuration values in code, which can be useful for:
- Setting different ports for multiple application instances
- Overriding configuration in tests
- Configuring applications dynamically
- Setting defaults that take precedence over environment variables
HTTP Port Override
The most common use case is overriding the HTTP port:
import "go.oease.dev/goe/v2"
func main() {
// Override HTTP port to 3000
app := goe.New(goe.Options{
WithHTTP: true,
HTTPPort: 3000,
Invokers: []any{
func(httpKernel contract.HTTPKernel) {
httpKernel.App().Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello from port 3000!")
})
},
},
})
goe.Run()
}
This will start the HTTP server on port 3000, regardless of the HTTP_PORT
environment variable.
General Configuration Overrides
You can override any configuration value using the ConfigOverrides
field:
import "go.oease.dev/goe/v2"
func main() {
app := goe.New(goe.Options{
WithHTTP: true,
WithDB: true,
ConfigOverrides: map[string]any{
"HTTP_PORT": 8090,
"DB_HOST": "localhost",
"DB_PORT": 5433,
"LOG_LEVEL": "debug",
"APP_NAME": "My Overridden App",
},
Invokers: []any{
func(config contract.Config, logger contract.Logger) {
// These values will be overridden
logger.Info("Configuration values",
"http_port", config.GetInt("HTTP_PORT"), // 8090
"db_host", config.GetString("DB_HOST"), // localhost
"db_port", config.GetInt("DB_PORT"), // 5433
"log_level", config.GetString("LOG_LEVEL"), // debug
"app_name", config.GetString("APP_NAME"), // My Overridden App
)
},
},
})
goe.Run()
}
Override Precedence
Configuration values are resolved in the following order (highest to lowest priority):
HTTPPort
field (only for HTTP_PORT)ConfigOverrides
map- System environment variables
.env.{GOE_ENV}
file.env.local
file.env
file
The HTTPPort
field takes precedence over ConfigOverrides
for the HTTP_PORT value:
app := goe.New(goe.Options{
WithHTTP: true,
HTTPPort: 7000, // This takes precedence
ConfigOverrides: map[string]any{
"HTTP_PORT": 8000, // This will be ignored
},
})
// The HTTP server will start on port 7000
Use Cases
Testing with Different Ports
func TestMultipleInstances(t *testing.T) {
// Start first instance on port 8001
app1 := goe.New(goe.Options{
WithHTTP: true,
HTTPPort: 8001,
})
// Start second instance on port 8002
app2 := goe.New(goe.Options{
WithHTTP: true,
HTTPPort: 8002,
})
// Test both instances independently
}
Environment-Specific Overrides
func main() {
var port int
var dbHost string
// Set different values based on environment
if os.Getenv("GOE_ENV") == "development" {
port = 8080
dbHost = "localhost"
} else {
port = 80
dbHost = "prod-db.example.com"
}
app := goe.New(goe.Options{
WithHTTP: true,
WithDB: true,
ConfigOverrides: map[string]any{
"HTTP_PORT": port,
"DB_HOST": dbHost,
},
})
goe.Run()
}
Configuration Factory Pattern
type AppConfig struct {
HTTPPort int
DBHost string
LogLevel string
}
func NewAppConfig(env string) *AppConfig {
switch env {
case "production":
return &AppConfig{
HTTPPort: 80,
DBHost: "prod-db.example.com",
LogLevel: "warn",
}
case "staging":
return &AppConfig{
HTTPPort: 8080,
DBHost: "staging-db.example.com",
LogLevel: "info",
}
default:
return &AppConfig{
HTTPPort: 8080,
DBHost: "localhost",
LogLevel: "debug",
}
}
}
func main() {
config := NewAppConfig(os.Getenv("GOE_ENV"))
app := goe.New(goe.Options{
WithHTTP: true,
WithDB: true,
HTTPPort: config.HTTPPort,
ConfigOverrides: map[string]any{
"DB_HOST": config.DBHost,
"LOG_LEVEL": config.LogLevel,
},
})
goe.Run()
}
Override Types
The ConfigOverrides
map accepts various types that will be properly converted:
app := goe.New(goe.Options{
ConfigOverrides: map[string]any{
"HTTP_PORT": 3000, // int
"DEBUG_MODE": true, // bool
"APP_NAME": "My App", // string
"TIMEOUT": 30.5, // float64
"CACHE_TTL": time.Minute * 15, // time.Duration
"TRUSTED_HOSTS": []string{"localhost"}, // []string
},
})
Configuration Methods
Basic Getters
config := goe.Config()
// String values
appName := config.GetString("APP_NAME")
// Integer values
port := config.GetInt("HTTP_PORT")
// Boolean values
debug := config.GetBool("DEBUG_MODE")
// Float values
timeout := config.GetFloat64("TIMEOUT")
// Duration values (parses strings like "30s", "5m", "1h")
cacheTTL := config.GetDuration("CACHE_TTL")
Getters with Defaults
config := goe.Config()
// With default values
httpPort := config.GetIntWithDefault("HTTP_PORT", 8080)
logLevel := config.GetStringWithDefault("LOG_LEVEL", "info")
enableMetrics := config.GetBoolWithDefault("ENABLE_METRICS", false)
requestTimeout := config.GetDurationWithDefault("REQUEST_TIMEOUT", 30*time.Second)
Checking if Keys Exist
config := goe.Config()
if config.Has("DATABASE_URL") {
databaseURL := config.GetString("DATABASE_URL")
// Use database URL
}
// Get all keys
keys := config.Keys()
for _, key := range keys {
fmt.Printf("Key: %s, Value: %s\n", key, config.GetString(key))
}
Configuration Patterns
Database Configuration
type DatabaseConfig struct {
Driver string
Host string
Port int
Database string
Username string
Password string
SSLMode string
}
func NewDatabaseConfig(config contract.Config) *DatabaseConfig {
return &DatabaseConfig{
Driver: config.GetString("DB_DRIVER"),
Host: config.GetString("DB_HOST"),
Port: config.GetInt("DB_PORT"),
Database: config.GetString("DB_NAME"),
Username: config.GetString("DB_USER"),
Password: config.GetString("DB_PASSWORD"),
SSLMode: config.GetStringWithDefault("DB_SSL_MODE", "disable"),
}
}
func (c *DatabaseConfig) ConnectionString() string {
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
c.Host, c.Port, c.Database, c.Username, c.Password, c.SSLMode)
}
HTTP Server Configuration
type HTTPConfig struct {
Host string
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
}
func NewHTTPConfig(config contract.Config) *HTTPConfig {
return &HTTPConfig{
Host: config.GetStringWithDefault("HTTP_HOST", "0.0.0.0"),
Port: config.GetIntWithDefault("HTTP_PORT", 8080),
ReadTimeout: config.GetDurationWithDefault("HTTP_READ_TIMEOUT", 30*time.Second),
WriteTimeout: config.GetDurationWithDefault("HTTP_WRITE_TIMEOUT", 30*time.Second),
IdleTimeout: config.GetDurationWithDefault("HTTP_IDLE_TIMEOUT", 60*time.Second),
}
}
Environment-Specific Configuration
Development Setup
# .env.development
APP_ENV=development
LOG_LEVEL=debug
LOG_FORMAT=console
# Enable debug features
DEBUG_MODE=true
ENABLE_PPROF=true
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
# Cache
CACHE_DRIVER=memory
Production Setup
# .env.production
APP_ENV=production
LOG_LEVEL=warn
LOG_FORMAT=json
# Production database
DB_HOST=prod-db.example.com
DB_PORT=5432
DB_NAME=myapp_prod
DB_SSL_MODE=require
# Redis cache
CACHE_DRIVER=redis
CACHE_REDIS_HOST=redis.example.com
CACHE_REDIS_PORT=6379
CACHE_REDIS_PASSWORD=secret
Running Different Environments
# Development (default)
go run main.go
# Production
GOE_ENV=production go run main.go
# Staging
GOE_ENV=staging go run main.go
# Testing
GOE_ENV=test go test ./...
Configuration in HTTP Handlers
import (
"github.com/gofiber/fiber/v3"
"go.oease.dev/goe/v2/contract"
)
func NewAppHandler(config contract.Config) *AppHandler {
return &AppHandler{config: config}
}
type AppHandler struct {
config contract.Config
}
func (h *AppHandler) GetInfo(c fiber.Ctx) error {
return c.JSON(fiber.Map{
"app_name": h.config.GetString("APP_NAME"),
"version": h.config.GetString("APP_VERSION"),
"environment": h.config.GetString("APP_ENV"),
})
}
func (h *AppHandler) GetHealth(c fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "healthy",
"env": h.config.GetString("APP_ENV"),
})
}
Configuration Validation
import (
"errors"
"go.oease.dev/goe/v2/contract"
)
func ValidateConfig(config contract.Config) error {
// Check required configuration
requiredKeys := []string{
"APP_NAME",
"DB_HOST",
"DB_PORT",
"DB_NAME",
"DB_USER",
}
for _, key := range requiredKeys {
if !config.Has(key) {
return fmt.Errorf("required configuration key missing: %s", key)
}
}
// Validate specific values
if config.GetInt("HTTP_PORT") <= 0 {
return errors.New("HTTP_PORT must be a positive integer")
}
if config.GetString("LOG_LEVEL") == "" {
return errors.New("LOG_LEVEL cannot be empty")
}
return nil
}
// Use in your application
func main() {
goe.New(goe.Options{
WithHTTP: true,
WithDB: true,
Invokers: []any{
func(config contract.Config) {
if err := ValidateConfig(config); err != nil {
goe.Log().Fatal("Configuration validation failed", "error", err)
}
},
},
})
goe.Run()
}
Best Practices
1. Use Environment Variables for Secrets
Never commit sensitive data to version control:
# Good - use environment variables
export DB_PASSWORD=secret123
export API_KEY=key123
# Bad - don't put secrets in .env files in version control
2. Provide Sensible Defaults
// Always provide defaults for non-critical configuration
httpPort := config.GetIntWithDefault("HTTP_PORT", 8080)
logLevel := config.GetStringWithDefault("LOG_LEVEL", "info")
3. Group Related Configuration
# Group related settings with prefixes
HTTP_HOST=0.0.0.0
HTTP_PORT=8080
HTTP_READ_TIMEOUT=30s
HTTP_WRITE_TIMEOUT=30s
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=postgres
4. Use .env.example
Create a .env.example
file with all required configuration keys:
# .env.example
APP_NAME=My GOE App
APP_VERSION=1.0.0
APP_ENV=development
HTTP_HOST=0.0.0.0
HTTP_PORT=8080
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=change_me
5. Validate Configuration Early
Validate configuration during application startup:
func main() {
goe.New(goe.Options{
WithHTTP: true,
Invokers: []any{
func(config contract.Config, logger contract.Logger) {
if !config.Has("DB_HOST") {
logger.Fatal("DB_HOST is required")
}
if config.GetInt("HTTP_PORT") <= 0 {
logger.Fatal("HTTP_PORT must be positive")
}
},
},
})
goe.Run()
}
Testing Configuration
import (
"testing"
"go.oease.dev/goe/v2/contract"
)
type MockConfig struct {
data map[string]string
}
func (c *MockConfig) GetString(key string) string {
return c.data[key]
}
func (c *MockConfig) GetInt(key string) int {
// Implementation for testing
return 0
}
func TestDatabaseService(t *testing.T) {
config := &MockConfig{
data: map[string]string{
"DB_HOST": "localhost",
"DB_PORT": "5432",
"DB_NAME": "testdb",
},
}
service := NewDatabaseService(config)
// Test service with mock config
}
Next Steps
- Logging - Learn about structured logging
- Database - Set up database connections
- HTTP Server - Configure HTTP server settings