config(configuration management)

Flexible configuration management system supporting multiple sources (files, environment variables, remote config centers) with unified reading, watching, and parsing interfaces. Protobuf is recommended for defining configuration structures.

The config module provides a flexible configuration management system that supports multiple configuration sources (files, environment variables, remote configuration centers) with unified reading, watching, and parsing interfaces. We recommend using Protocol Buffers (protobuf) to define configuration structures for better type safety, schema validation, and cross-language compatibility.

Features

  • Multiple configuration sources support (files, environment variables, remote config centers)
  • Unified configuration interface for consistent access patterns
  • Real-time configuration watching with observer pattern
  • Automatic configuration merging from multiple sources
  • Type-safe configuration scanning to structs
  • Hierarchical configuration with key-value access
  • Configuration caching for improved performance
  • Extensible source system for custom configuration providers

First, define your configuration structure using Protocol Buffers:

 1// config.proto
 2syntax = "proto3";
 3
 4package config;
 5
 6option go_package = "github.com/yourproject/config";
 7
 8message AppConfig {
 9  ServerConfig server = 1;
10  DatabaseConfig database = 2;
11}
12
13message ServerConfig {
14  string host = 1;
15  int32 port = 2;
16}
17
18message DatabaseConfig {
19  string host = 1;
20  int32 port = 2;
21  string username = 3;
22  string password = 4;
23}

Then use it in your Go application:

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "log"
 7
 8    "github.com/crazyfrankie/frx/config"
 9    "github.com/crazyfrankie/frx/config/file"
10    "github.com/crazyfrankie/frx/config/env"
11    
12    // Import your generated protobuf config
13    configpb "github.com/yourproject/config"
14)
15
16func main() {
17    // Create configuration with multiple sources
18    c := config.New(
19        config.WithSource(
20            file.NewSource("config.yaml"),
21            env.NewSource("APP_"),
22        ),
23    )
24    defer c.Close()
25
26    // Load configuration
27    if err := c.Load(); err != nil {
28        log.Fatal(err)
29    }
30
31    // Scan configuration into protobuf struct
32    var cfg configpb.AppConfig
33    if err := c.Scan(&cfg); err != nil {
34        log.Fatal(err)
35    }
36
37    fmt.Printf("Server: %s:%d\n", cfg.Server.Host, cfg.Server.Port)
38    fmt.Printf("Database: %s:%d\n", cfg.Database.Host, cfg.Database.Port)
39}

Alternative: Traditional Struct Usage

If you prefer not to use protobuf, you can still use traditional Go structs:

 1type AppConfig struct {
 2    Server struct {
 3        Host string `json:"host"`
 4        Port int    `json:"port"`
 5    } `json:"server"`
 6    Database struct {
 7        Host     string `json:"host"`
 8        Port     int    `json:"port"`
 9        Username string `json:"username"`
10        Password string `json:"password"`
11    } `json:"database"`
12}

Configuration Sources

File Source

 1import "github.com/crazyfrankie/frx/config/file"
 2
 3// YAML file source
 4yamlSource := file.NewSource("config.yaml")
 5
 6// JSON file source  
 7jsonSource := file.NewSource("config.json")
 8
 9// Multiple file sources
10c := config.New(
11    config.WithSource(
12        file.NewSource("config.yaml"),
13        file.NewSource("config.local.yaml"), // Override with local config
14    ),
15)

Environment Variable Source

 1import "github.com/crazyfrankie/frx/config/env"
 2
 3// Environment variables with prefix
 4envSource := env.NewSource("APP_")
 5
 6c := config.New(
 7    config.WithSource(envSource),
 8)
 9
10// Environment variables will be mapped:
11// APP_SERVER_HOST -> server.host
12// APP_DATABASE_PORT -> database.port

Configuration Watching

 1func main() {
 2    c := config.New(
 3        config.WithSource(file.NewSource("config.yaml")),
 4    )
 5    defer c.Close()
 6
 7    if err := c.Load(); err != nil {
 8        log.Fatal(err)
 9    }
10
11    // Watch for configuration changes
12    if err := c.Watch("server", func(key string, value config.Value) {
13        fmt.Printf("Configuration changed - %s: %v\n", key, value)
14        
15        // Reload application configuration
16        var cfg AppConfig
17        if err := c.Scan(&cfg); err != nil {
18            log.Printf("Failed to reload config: %v", err)
19            return
20        }
21        
22        // Apply new configuration
23        applyNewConfig(cfg)
24    }); err != nil {
25        log.Fatal(err)
26    }
27
28    // Keep application running
29    select {}
30}
31
32func applyNewConfig(cfg AppConfig) {
33    // Restart server with new configuration
34    fmt.Printf("Applying new config: %+v\n", cfg)
35}

Value Access and Type Conversion

 1func main() {
 2    c := config.New(
 3        config.WithSource(file.NewSource("config.yaml")),
 4    )
 5    defer c.Close()
 6
 7    if err := c.Load(); err != nil {
 8        log.Fatal(err)
 9    }
10
11    // Get values by key
12    serverHost := c.Value("server.host")
13    if serverHost == nil {
14        log.Fatal("server.host not found")
15    }
16
17    // Type-safe value conversion
18    host, err := serverHost.String()
19    if err != nil {
20        log.Fatal(err)
21    }
22
23    port, err := c.Value("server.port").Int()
24    if err != nil {
25        log.Fatal(err)
26    }
27
28    enabled, err := c.Value("features.enabled").Bool()
29    if err != nil {
30        log.Fatal(err)
31    }
32
33    fmt.Printf("Server: %s:%d, Features enabled: %v\n", host, port, enabled)
34}

Configuration Merging

 1func main() {
 2    // Configuration sources are merged in order
 3    // Later sources override earlier ones
 4    c := config.New(
 5        config.WithSource(
 6            file.NewSource("config.default.yaml"), // Default configuration
 7            file.NewSource("config.yaml"),         // Environment-specific config
 8            env.NewSource("APP_"),                  // Environment variables (highest priority)
 9        ),
10    )
11    defer c.Close()
12
13    if err := c.Load(); err != nil {
14        log.Fatal(err)
15    }
16
17    // The final configuration is a merge of all sources
18    var cfg AppConfig
19    if err := c.Scan(&cfg); err != nil {
20        log.Fatal(err)
21    }
22}

Custom Configuration Source

 1import "github.com/crazyfrankie/frx/config"
 2
 3// Implement custom source
 4type customSource struct {
 5    data map[string]interface{}
 6}
 7
 8func (s *customSource) Load() ([]*config.KeyValue, error) {
 9    var kvs []*config.KeyValue
10    for k, v := range s.data {
11        kvs = append(kvs, &config.KeyValue{
12            Key:   k,
13            Value: []byte(fmt.Sprintf("%v", v)),
14        })
15    }
16    return kvs, nil
17}
18
19func (s *customSource) Watch() (config.Watcher, error) {
20    // Implement watcher if needed
21    return nil, nil
22}
23
24func NewCustomSource(data map[string]interface{}) config.Source {
25    return &customSource{data: data}
26}
27
28// Usage
29customData := map[string]interface{}{
30    "app.name":    "MyApp",
31    "app.version": "1.0.0",
32}
33
34c := config.New(
35    config.WithSource(NewCustomSource(customData)),
36)