This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Config Package

API reference for rivaas.dev/config - Configuration management for Go applications

This is the API reference for the rivaas.dev/config package. For learning-focused documentation, see the Config Guide.

Package Information

Package Overview

The config package provides powerful configuration management for Go applications with support for multiple sources, formats, and validation strategies.

Core Features

  • Multiple configuration sources (files, environment variables, remote sources)
  • Format-agnostic with built-in JSON, YAML, and TOML support
  • Hierarchical configuration merging
  • Automatic struct binding with type safety
  • Multiple validation strategies
  • Thread-safe operations
  • Nil-safe getter methods

Architecture

The package is organized into several key components:

Main Package (rivaas.dev/config)

Core configuration management including:

  • Config struct - Main configuration container
  • New() / MustNew() - Configuration initialization
  • Getter methods - Type-safe value retrieval
  • Load() / Dump() - Loading and saving configuration

Sub-packages

  • codec - Format encoding/decoding (JSON, YAML, TOML, etc.)
  • source - Configuration sources (file, environment, Consul, etc.)
  • dumper - Configuration output destinations

Quick API Index

Configuration Creation

cfg, err := config.New(options...)     // With error handling
cfg := config.MustNew(options...)      // Panics on error

Loading Configuration

err := cfg.Load(ctx context.Context)

Accessing Values

// Direct access (returns zero values for missing keys)
value := cfg.String("key")
value := cfg.Int("key")
value := cfg.Bool("key")

// With defaults
value := cfg.StringOr("key", "default")
value := cfg.IntOr("key", 8080)

// With error handling
value, err := config.GetE[Type](cfg, "key")

Dumping Configuration

err := cfg.Dump(ctx context.Context)

Reference Pages

API Reference

Complete documentation of the Config struct and all methods including:

  • Configuration lifecycle methods
  • All getter method signatures
  • Error types and handling
  • Nil-safety guarantees

Options

Comprehensive list of all configuration options:

  • Source options (WithFile, WithEnv, WithConsul, etc.)
  • Validation options (WithBinding, WithValidator, WithJSONSchema)
  • Dumper options (WithFileDumper, WithDumper)

Codecs

Built-in and custom codec documentation:

  • Format codecs (JSON, YAML, TOML, EnvVar)
  • Caster codecs (Int, Bool, Duration, Time, etc.)
  • Creating custom codecs
  • File extension auto-detection

Troubleshooting

Common issues and solutions:

  • Configuration not loading
  • Struct not populating
  • Environment variable mapping
  • Performance considerations
  • Thread-safety information

Type Reference

Config

type Config struct {
    // contains filtered or unexported fields
}

Main configuration container. Thread-safe for concurrent access.

ConfigError

type ConfigError struct {
    Source    string // Where the error occurred
    Field     string // Specific field with error
    Operation string // Operation being performed
    Err       error  // Underlying error
}

Error type for configuration operations with detailed context.

Option

type Option func(*Config) error

Configuration option function type used with New() and MustNew().

Common Patterns

Basic Usage

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
)
cfg.Load(context.Background())

port := cfg.Int("server.port")

With Struct Binding

type AppConfig struct {
    Server struct {
        Port int `config:"port"`
    } `config:"server"`
}

var appConfig AppConfig
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithBinding(&appConfig),
)
cfg.Load(context.Background())

With Validation

func (c *AppConfig) Validate() error {
    if c.Server.Port <= 0 {
        return errors.New("port must be positive")
    }
    return nil
}

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithBinding(&appConfig),  // Validation runs after binding
)

Thread Safety

The Config type is thread-safe for:

  • Concurrent Load() operations
  • Concurrent getter operations
  • Mixed Load() and getter operations

Not thread-safe for:

  • Concurrent modification of the same configuration instance during initialization

Performance Notes

  • Getter methods are O(1) for simple keys, O(n) for nested dot notation paths
  • Load performance depends on source count and data size
  • Struct binding uses reflection, minimal overhead for most applications
  • Validation overhead depends on validation complexity

Version Compatibility

The config package follows semantic versioning. The API is stable for the v1 series.

Minimum Go version: 1.25

Next Steps

For learning-focused guides, see the Configuration Guide.

1 - API Reference

Complete API documentation for the Config type and methods

Complete API reference for the Config struct and all its methods.

Types

Config

type Config struct {
    // contains filtered or unexported fields
}

Main configuration container. Thread-safe for concurrent read operations and loading.

Key properties:

  • Thread-safe for concurrent Load() and getter operations.
  • Nil-safe. All getter methods handle nil instances gracefully.
  • Hierarchical data storage with dot notation support.

ConfigError

type ConfigError struct {
    Source    string // Where the error occurred (e.g., "source[0]", "json-schema")
    Field     string // Specific field with the error (optional)
    Operation string // Operation being performed (e.g., "load", "validate")
    Err       error  // Underlying error
}

Error type providing detailed context about configuration errors.

Example error messages:

config error in source[0] during load: file not found: config.yaml
config error in json-schema during validate: server.port: must be >= 1
config error in binding during bind: failed to decode configuration

Initialization Functions

New

func New(options ...Option) (*Config, error)

Creates a new Config instance with the given options. Returns an error if any option fails.

Parameters:

  • options - Variable number of Option functions.

Returns:

  • *Config - Initialized configuration instance.
  • error - Error if initialization fails.

Example:

cfg, err := config.New(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
)
if err != nil {
    log.Fatalf("failed to create config: %v", err)
}

Use when: You need explicit error handling. Recommended for libraries.

MustNew

func MustNew(options ...Option) *Config

Creates a new Config instance with the given options. Panics if any option fails.

Parameters:

  • options - Variable number of Option functions

Returns:

  • *Config - Initialized configuration instance

Panics: If any option returns an error

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
)

Use when: In main() or initialization code where panic is acceptable.

Lifecycle Methods

Load

func (c *Config) Load(ctx context.Context) error

Loads configuration from all configured sources, merges them, and runs validation.

Parameters:

  • ctx - Context for cancellation and deadlines (must not be nil)

Returns:

  • error - ConfigError if loading, merging, or validation fails

Behavior:

  1. Loads data from all sources sequentially
  2. Merges data hierarchically (later sources override earlier ones)
  3. Runs JSON Schema validation (if configured)
  4. Runs custom validation functions (if configured)
  5. Binds to struct (if configured)
  6. Runs struct Validate() method (if implemented)

Example:

if err := cfg.Load(context.Background()); err != nil {
    log.Fatalf("failed to load config: %v", err)
}

Thread-safety: Safe for concurrent calls (uses internal locking).

Dump

func (c *Config) Dump(ctx context.Context) error

Writes the current configuration state to all configured dumpers.

Parameters:

  • ctx - Context for cancellation and deadlines (must not be nil)

Returns:

  • error - Error if any dumper fails

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithFileDumper("output.yaml"),
)
cfg.Load(context.Background())
cfg.Dump(context.Background())  // Writes to output.yaml

Use cases: Debugging, configuration snapshots, generating configuration files.

Getter Methods

Get

func (c *Config) Get(key string) any

Retrieves the value at the given key path. Returns nil for missing keys.

Parameters:

  • key - Dot-separated path (e.g., “server.port”)

Returns:

  • any - Value at the key, or nil if not found

Nil-safe: Returns nil if Config instance is nil.

Example:

value := cfg.Get("server.port")
if port, ok := value.(int); ok {
    fmt.Printf("Port: %d\n", port)
}

String

func (c *Config) String(key string) string

Retrieves a string value at the given key.

Returns: Empty string "" if key not found or on nil instance.

Example:

host := cfg.String("server.host")  // "" if missing

Int

func (c *Config) Int(key string) int

Retrieves an integer value at the given key.

Returns: 0 if key not found or on nil instance.

Int64

func (c *Config) Int64(key string) int64

Retrieves an int64 value at the given key.

Returns: 0 if key not found or on nil instance.

Float64

func (c *Config) Float64(key string) float64

Retrieves a float64 value at the given key.

Returns: 0.0 if key not found or on nil instance.

Bool

func (c *Config) Bool(key string) bool

Retrieves a boolean value at the given key.

Returns: false if key not found or on nil instance.

Duration

func (c *Config) Duration(key string) time.Duration

Retrieves a time.Duration value at the given key. Supports duration strings like “30s”, “5m”, “1h”.

Returns: 0 if key not found or on nil instance.

Example:

timeout := cfg.Duration("server.timeout")  // Parses "30s" to 30 * time.Second

Time

func (c *Config) Time(key string) time.Time

Retrieves a time.Time value at the given key.

Returns: Zero time (time.Time{}) if key not found or on nil instance.

StringSlice

func (c *Config) StringSlice(key string) []string

Retrieves a string slice at the given key.

Returns: Empty slice []string{} (not nil) if key not found or on nil instance.

Example:

hosts := cfg.StringSlice("servers")  // []string{} if missing

IntSlice

func (c *Config) IntSlice(key string) []int

Retrieves an integer slice at the given key.

Returns: Empty slice []int{} (not nil) if key not found or on nil instance.

StringMap

func (c *Config) StringMap(key string) map[string]any

Retrieves a map at the given key.

Returns: Empty map map[string]any{} (not nil) if key not found or on nil instance.

Example:

metadata := cfg.StringMap("metadata")  // map[string]any{} if missing

Getter Methods with Defaults

StringOr

func (c *Config) StringOr(key, defaultVal string) string

Retrieves a string value or returns the default if not found.

Example:

host := cfg.StringOr("server.host", "localhost")

IntOr

func (c *Config) IntOr(key string, defaultVal int) int

Retrieves an integer value or returns the default if not found.

Example:

port := cfg.IntOr("server.port", 8080)

Int64Or

func (c *Config) Int64Or(key string, defaultVal int64) int64

Retrieves an int64 value or returns the default if not found.

Float64Or

func (c *Config) Float64Or(key string, defaultVal float64) float64

Retrieves a float64 value or returns the default if not found.

BoolOr

func (c *Config) BoolOr(key string, defaultVal bool) bool

Retrieves a boolean value or returns the default if not found.

Example:

debug := cfg.BoolOr("debug", false)

DurationOr

func (c *Config) DurationOr(key string, defaultVal time.Duration) time.Duration

Retrieves a duration value or returns the default if not found.

Example:

timeout := cfg.DurationOr("timeout", 30*time.Second)

TimeOr

func (c *Config) TimeOr(key string, defaultVal time.Time) time.Time

Retrieves a time.Time value or returns the default if not found.

StringSliceOr

func (c *Config) StringSliceOr(key string, defaultVal []string) []string

Retrieves a string slice or returns the default if not found.

IntSliceOr

func (c *Config) IntSliceOr(key string, defaultVal []int) []int

Retrieves an integer slice or returns the default if not found.

StringMapOr

func (c *Config) StringMapOr(key string, defaultVal map[string]any) map[string]any

Retrieves a map or returns the default if not found.

Generic Getter Functions

GetE

func GetE[T any](c *Config, key string) (T, error)

Generic getter that returns the value and an error. Useful for custom types and explicit error handling.

Type parameters:

  • T - Target type

Parameters:

  • c - Config instance
  • key - Dot-separated path

Returns:

  • T - Value at the key (zero value if error)
  • error - Error if key not found, type mismatch, or nil instance

Example:

port, err := config.GetE[int](cfg, "server.port")
if err != nil {
    log.Printf("invalid port: %v", err)
    port = 8080
}

// Custom type
type DatabaseConfig struct {
    Host string
    Port int
}

dbConfig, err := config.GetE[DatabaseConfig](cfg, "database")

GetOr

func GetOr[T any](c *Config, key string, defaultVal T) T

Generic getter that returns the value or a default if not found.

Example:

port := config.GetOr(cfg, "server.port", 8080)

Get

func Get[T any](c *Config, key string) T

Generic getter that returns the value or zero value if not found.

Example:

port := config.Get[int](cfg, "server.port")  // 0 if missing

Data Access Methods

Values

func (c *Config) Values() *map[string]any

Returns a pointer to the internal configuration map.

Returns: nil if Config instance is nil

Warning: Direct modification of the returned map is not recommended. Use for read-only operations.

Example:

values := cfg.Values()
if values != nil {
    fmt.Printf("Config data: %+v\n", *values)
}

Nil-Safety Guarantees

All getter methods handle nil Config instances gracefully:

var cfg *config.Config  // nil

// Short methods return zero values
cfg.String("key")       // Returns ""
cfg.Int("key")          // Returns 0
cfg.Bool("key")         // Returns false
cfg.StringSlice("key")  // Returns []string{}
cfg.StringMap("key")    // Returns map[string]any{}

// Error methods return errors
port, err := config.GetE[int](cfg, "key")
// err: "config instance is nil"

Thread Safety

Thread-safe operations:

  • Load() - Uses internal locking
  • All getter methods - Read-only operations are safe
  • Multiple goroutines can call Load() and getters concurrently

Not thread-safe:

  • Concurrent modification during initialization
  • Direct modification of values returned by Values()

Error Handling Patterns

Pattern 1: Simple Access

port := cfg.Int("server.port")  // Use zero value as implicit default

Pattern 2: Explicit Defaults

port := cfg.IntOr("server.port", 8080)  // Explicit default

Pattern 3: Error Handling

port, err := config.GetE[int](cfg, "server.port")
if err != nil {
    return fmt.Errorf("invalid port: %w", err)
}

Pattern 4: Load Errors

if err := cfg.Load(context.Background()); err != nil {
    var configErr *config.ConfigError
    if errors.As(err, &configErr) {
        log.Printf("Config error in %s during %s: %v",
            configErr.Source, configErr.Operation, configErr.Err)
    }
    return err
}

Performance Characteristics

OperationComplexityNotes
Get(key)O(n)n = depth of dot notation path
String(key), Int(key), etc.O(n)Uses Get() internally
Load()O(s × m)s = number of sources, m = data size
Dump()O(d × m)d = number of dumpers, m = data size

Next Steps

2 - Options Reference

Complete reference for all configuration option functions

Comprehensive documentation of all option functions used to configure Config instances.

Option Type

type Option func(*Config) error

Options are functions that configure a Config instance during initialization. They are passed to New() or MustNew().

Environment Variable Expansion

All path-based options (WithFile, WithFileAs, WithConsul, WithConsulAs, WithFileDumper, WithFileDumperAs) support environment variable expansion in paths. This makes it easy to use different paths based on your environment.

Supported syntax:

  • ${VAR} - Braced variable name
  • $VAR - Simple variable name

Note: Shell-style defaults like ${VAR:-default} are NOT supported. Set defaults in your code before calling the option.

Examples:

// Environment-based Consul path
config.WithConsul("${APP_ENV}/service.yaml")
// When APP_ENV=production, expands to: "production/service.yaml"

// Config directory from environment
config.WithFile("${CONFIG_DIR}/app.yaml")
// When CONFIG_DIR=/etc/myapp, expands to: "/etc/myapp/app.yaml"

// Multiple variables
config.WithFile("${REGION}/${ENV}/settings.yaml")
// When REGION=us-west and ENV=staging, expands to: "us-west/staging/settings.yaml"

// Output directory
config.WithFileDumper("${LOG_DIR}/effective-config.yaml")
// When LOG_DIR=/var/log, expands to: "/var/log/effective-config.yaml"

Handling unset variables:

If an environment variable is not set, it expands to an empty string:

// If APP_ENV is not set:
config.WithConsul("${APP_ENV}/service.yaml")  // Expands to: "/service.yaml"

To provide defaults, set them in your code:

if os.Getenv("APP_ENV") == "" {
    os.Setenv("APP_ENV", "development")
}
config.WithConsul("${APP_ENV}/service.yaml")  // Uses "development" if not set

Source Options

Source options specify where configuration data comes from.

WithFile

func WithFile(path string) Option

Loads configuration from a file with automatic format detection based on extension.

Parameters:

  • path - Path to configuration file.

Supported extensions:

  • .json - JSON format.
  • .yaml, .yml - YAML format.
  • .toml - TOML format.

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithFile("config.json"),
)

Error conditions:

  • File does not exist (error occurs during Load(), not initialization)
  • Extension not recognized

WithFileAs

func WithFileAs(path string, codecType codec.Type) Option

Loads configuration from a file with explicit format specification.

Parameters:

  • path - Path to configuration file.
  • codecType - Codec type like codec.TypeYAML or codec.TypeJSON.

Example:

cfg := config.MustNew(
    config.WithFileAs("config.txt", codec.TypeYAML),
    config.WithFileAs("settings.conf", codec.TypeJSON),
)

Use when: File extension doesn’t match its format.

WithEnv

func WithEnv(prefix string) Option

Loads configuration from environment variables with the given prefix.

Parameters:

  • prefix - Prefix to filter environment variables (e.g., “APP_”, “MYAPP_”)

Naming convention:

  • PREFIX_KEYkey
  • PREFIX_SECTION_KEYsection.key
  • PREFIX_A_B_Ca.b.c

Example:

cfg := config.MustNew(
    config.WithEnv("MYAPP_"),
)

// Environment: MYAPP_SERVER_PORT=8080
// Maps to: server.port = 8080

See also: Environment Variables Guide

WithConsul

func WithConsul(path string) Option

Loads configuration from HashiCorp Consul. The format is detected from the file extension.

Works without Consul: If CONSUL_HTTP_ADDR isn’t set, this option does nothing. This means you can run your app locally without Consul. When you deploy to production, just set the environment variable and Consul will be used.

Parameters:

  • path - Consul key path (format detected from extension)

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithConsul("production/service.json"),  // Skipped in dev, used in prod
)

Environment variables:

  • CONSUL_HTTP_ADDR - Consul server address (required for Consul to work)
  • CONSUL_HTTP_TOKEN - Access token for authentication (optional)

WithConsulAs

func WithConsulAs(path string, codecType codec.Type) Option

Loads configuration from Consul with explicit format. Use this when the key path doesn’t have an extension.

Works without Consul: Like WithConsul, this option does nothing if CONSUL_HTTP_ADDR isn’t set. Your code works the same in dev and prod.

Parameters:

  • path - Consul key path
  • codecType - Codec type (like codec.TypeYAML or codec.TypeJSON)

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithConsulAs("config/app", codec.TypeYAML),  // No extension in key
)

Environment variables:

  • CONSUL_HTTP_ADDR - Consul server address (required for Consul to work)
  • CONSUL_HTTP_TOKEN - Access token for authentication (optional)

WithContent

func WithContent(data []byte, codecType codec.Type) Option

Loads configuration from a byte slice.

Parameters:

  • data - Configuration data as bytes
  • codecType - Codec type for decoding

Example:

configData := []byte(`{"server": {"port": 8080}}`)
cfg := config.MustNew(
    config.WithContent(configData, codec.TypeJSON),
)

Use cases:

  • Testing
  • Dynamic configuration
  • Embedded configuration

WithSource

func WithSource(loader Source) Option

Adds a custom configuration source.

Parameters:

  • loader - Custom source implementing the Source interface

Source interface:

type Source interface {
    Load(ctx context.Context) (map[string]any, error)
}

Example:

type CustomSource struct{}

func (s *CustomSource) Load(ctx context.Context) (map[string]any, error) {
    return map[string]any{"key": "value"}, nil
}

cfg := config.MustNew(
    config.WithSource(&CustomSource{}),
)

Validation Options

Validation options enable configuration validation.

WithBinding

func WithBinding(v any) Option

Binds configuration to a Go struct and optionally validates it.

Parameters:

  • v - Pointer to struct to bind configuration to

Example:

type Config struct {
    Port int `config:"port"`
}

var cfg Config
config := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithBinding(&cfg),
)

Validation: If the struct implements Validate() error, it will be called after binding.

Requirements:

  • Must pass a pointer to the struct
  • Struct fields must have config:"name" tags

See also: Struct Binding Guide

WithTag

func WithTag(tagName string) Option

Changes the struct tag name used for binding (default: “config”).

Parameters:

  • tagName - Tag name to use instead of “config”

Example:

type Config struct {
    Port int `yaml:"port"`
}

var cfg Config
config := config.MustNew(
    config.WithTag("yaml"),
    config.WithBinding(&cfg),
)

Use when: You want to reuse existing struct tags (e.g., json, yaml).

WithValidator

func WithValidator(fn func(map[string]any) error) Option

Registers a custom validation function for the configuration map.

Parameters:

  • fn - Validation function that receives the merged configuration

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithValidator(func(data map[string]any) error {
        port, ok := data["port"].(int)
        if !ok || port <= 0 {
            return errors.New("port must be a positive integer")
        }
        return nil
    }),
)

Timing: Validation runs after sources are merged, before struct binding.

Multiple validators: You can register multiple validators; all will be executed.

WithJSONSchema

func WithJSONSchema(schema []byte) Option

Validates configuration against a JSON Schema.

Parameters:

  • schema - JSON Schema as bytes

Example:

schemaBytes, _ := os.ReadFile("schema.json")
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithJSONSchema(schemaBytes),
)

Schema validation:

See also: Validation Guide

Dumper Options

Dumper options specify where to write configuration.

WithFileDumper

func WithFileDumper(path string) Option

Writes configuration to a file with automatic format detection.

Parameters:

  • path - Output file path (format detected from extension)

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
    config.WithFileDumper("effective-config.yaml"),
)

cfg.Load(context.Background())
cfg.Dump(context.Background())  // Writes to effective-config.yaml

Default permissions: 0644 (owner read/write, group/others read)

WithFileDumperAs

func WithFileDumperAs(path string, codecType codec.Type) Option

Writes configuration to a file with explicit format specification.

Parameters:

  • path - Output file path
  • codecType - Codec type for encoding

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithFileDumperAs("output.json", codec.TypeJSON),
)

WithDumper

func WithDumper(dumper Dumper) Option

Adds a custom configuration dumper.

Parameters:

  • dumper - Custom dumper implementing the Dumper interface

Dumper interface:

type Dumper interface {
    Dump(ctx context.Context, data map[string]any) error
}

Example:

type CustomDumper struct{}

func (d *CustomDumper) Dump(ctx context.Context, data map[string]any) error {
    // Write data somewhere
    return nil
}

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithDumper(&CustomDumper{}),
)

Option Composition

Options are applied in the order they are passed to New() or MustNew():

cfg := config.MustNew(
    // 1. Load base config
    config.WithFile("config.yaml"),
    
    // 2. Load environment-specific config
    config.WithFile("config.prod.yaml"),
    
    // 3. Override with environment variables (highest priority)
    config.WithEnv("APP_"),
    
    // 4. Set up validation
    config.WithJSONSchema(schemaBytes),
    config.WithValidator(customValidation),
    
    // 5. Bind to struct
    config.WithBinding(&appConfig),
    
    // 6. Set up dumper
    config.WithFileDumper("effective-config.yaml"),
)

Source Precedence

When multiple sources are configured, later sources override earlier ones:

cfg := config.MustNew(
    config.WithFile("config.yaml"),      // Priority 1 (lowest)
    config.WithFile("config.prod.yaml"), // Priority 2
    config.WithEnv("APP_"),              // Priority 3 (highest)
)

Validation Order

Validation happens in this sequence during Load():

  1. Load and merge all sources
  2. JSON Schema validation (if configured)
  3. Custom validation functions (if configured)
  4. Struct binding (if configured)
  5. Struct Validate() method (if implemented)

Common Patterns

Pattern 1: Basic Configuration

cfg := config.MustNew(
    config.WithFile("config.yaml"),
)

Pattern 2: Environment Override

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
)

Pattern 3: Multi-Environment

env := os.Getenv("APP_ENV")
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithFile("config."+env+".yaml"),
    config.WithEnv("APP_"),
)

Pattern 4: With Validation

var appConfig AppConfig
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
    config.WithBinding(&appConfig),
)

Pattern 5: Production Setup

var appConfig AppConfig
schemaBytes, _ := os.ReadFile("schema.json")

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
    config.WithJSONSchema(schemaBytes),
    config.WithBinding(&appConfig),
    config.WithFileDumper("effective-config.yaml"),
)

Next Steps

3 - Codecs Reference

Built-in codecs for configuration format support and type conversion

Complete reference for built-in codecs and guidance on creating custom codecs.

Codec Interface

type Codec interface {
    Encode(v any) ([]byte, error)
    Decode(data []byte, v any) error
}

Codecs handle encoding and decoding of configuration data between different formats.

Built-in Format Codecs

JSON Codec

Type: codec.TypeJSON
Import: rivaas.dev/config/codec

Handles JSON format encoding and decoding.

Capabilities:

  • ✅ Encode
  • ✅ Decode

File extensions:

  • .json

Example:

import "rivaas.dev/config/codec"

cfg := config.MustNew(
    config.WithFileAs("config.txt", codec.TypeJSON),
)

Features:

  • Standard Go encoding/json implementation
  • Preserves JSON types (numbers, strings, booleans, arrays, objects)
  • Pretty-printed output when encoding

YAML Codec

Type: codec.TypeYAML
Import: rivaas.dev/config/codec

Handles YAML format encoding and decoding.

Capabilities:

  • ✅ Encode
  • ✅ Decode

File extensions:

  • .yaml
  • .yml

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
)

Features:

  • Uses gopkg.in/yaml.v3
  • Supports YAML 1.2 features
  • Handles anchors and aliases
  • Preserves indentation on encoding

Common YAML types:

string_value: "hello"
number_value: 42
boolean_value: true
duration_value: 30s
list_value:
  - item1
  - item2
map_value:
  key1: value1
  key2: value2

TOML Codec

Type: codec.TypeTOML
Import: rivaas.dev/config/codec

Handles TOML format encoding and decoding.

Capabilities:

  • ✅ Encode
  • ✅ Decode

File extensions:

  • .toml

Example:

cfg := config.MustNew(
    config.WithFile("config.toml"),
)

Features:

Sample TOML:

[server]
host = "localhost"
port = 8080

[database]
host = "db.example.com"
port = 5432

Environment Variable Codec

Type: codec.TypeEnvVar
Import: rivaas.dev/config/codec

Handles environment variable format.

Capabilities:

  • ❌ Encode (returns error)
  • ✅ Decode

Example:

cfg := config.MustNew(
    config.WithEnv("APP_"),
)

Format:

PREFIX_SECTION_KEY=value
PREFIX_A_B_C=nested

Transformation:

  • Strips prefix
  • Converts to lowercase
  • Splits by underscores
  • Creates nested structure

See: Environment Variables Guide

Built-in Caster Codecs

Caster codecs provide automatic type conversion for getter methods.

Boolean Caster

Type: codec.TypeCasterBool

Converts values to bool.

Supported inputs:

  • true, "true", "True", "TRUE", 1, "1"true
  • false, "false", "False", "FALSE", 0, "0"false

Example:

debug := cfg.Bool("debug")  // Uses BoolCaster internally

Integer Casters

Convert values to integer types.

TypeCodecTarget Type
codec.TypeCasterIntIntint
codec.TypeCasterInt8Int8int8
codec.TypeCasterInt16Int16int16
codec.TypeCasterInt32Int32int32
codec.TypeCasterInt64Int64int64

Supported inputs:

  • Integer values: 42, 100
  • String integers: "42", "100"
  • Float values: 42.042
  • String floats: "42.0"42

Example:

port := cfg.Int("server.port")      // Uses IntCaster
timeout := cfg.Int64("timeout_ms")  // Uses Int64Caster

Unsigned Integer Casters

Convert values to unsigned integer types.

TypeCodecTarget Type
codec.TypeCasterUintUintuint
codec.TypeCasterUint8Uint8uint8
codec.TypeCasterUint16Uint16uint16
codec.TypeCasterUint32Uint32uint32
codec.TypeCasterUint64Uint64uint64

Supported inputs:

  • Positive integers: 42, 100
  • String integers: "42", "100"

Float Casters

Convert values to floating-point types.

TypeCodecTarget Type
codec.TypeCasterFloat32Float32float32
codec.TypeCasterFloat64Float64float64

Supported inputs:

  • Float values: 3.14, 2.5
  • String floats: "3.14", "2.5"
  • Integer values: 4242.0
  • String integers: "42"42.0

Example:

ratio := cfg.Float64("ratio")

String Caster

Type: codec.TypeCasterString

Converts any value to string.

Supported inputs:

  • String values: "hello""hello"
  • Numbers: 42"42"
  • Booleans: true"true"
  • Any value with String() method

Example:

value := cfg.String("key")  // Uses StringCaster internally

Time Caster

Type: codec.TypeCasterTime

Converts values to time.Time.

Supported inputs:

  • RFC3339 strings: "2025-01-01T00:00:00Z"
  • ISO8601 strings: "2025-01-01T00:00:00+00:00"
  • Unix timestamps: 1672531200

Example:

createdAt := cfg.Time("created_at")

Formats tried (in order):

  1. time.RFC3339 - "2006-01-02T15:04:05Z07:00"
  2. time.RFC3339Nano - "2006-01-02T15:04:05.999999999Z07:00"
  3. "2006-01-02" - Date only
  4. Unix timestamp (integer)

Duration Caster

Type: codec.TypeCasterDuration

Converts values to time.Duration.

Supported inputs:

  • Duration strings: "30s", "5m", "1h", "2h30m"
  • Integer nanoseconds: 3000000000030s
  • Float seconds: 2.52.5s

Example:

timeout := cfg.Duration("timeout")  // "30s" → 30 * time.Second

Duration units:

  • ns - nanoseconds
  • us or µs - microseconds
  • ms - milliseconds
  • s - seconds
  • m - minutes
  • h - hours

Codec Capabilities Table

CodecEncodeDecodeAuto-DetectExtensions
JSON.json
YAML.yaml, .yml
TOML.toml
EnvVar-
Bool-
Int*-
Uint*-
Float*-
String-
Time-
Duration-

Format Auto-Detection

The config package automatically detects formats based on file extensions:

cfg := config.MustNew(
    config.WithFile("config.json"),  // Auto-detects JSON
    config.WithFile("config.yaml"),  // Auto-detects YAML
    config.WithFile("config.toml"),  // Auto-detects TOML
)

Detection rules:

  1. Check file extension
  2. Look up registered decoder for that extension
  3. Use codec if found, error if not

Override auto-detection:

cfg := config.MustNew(
    config.WithFileAs("settings.txt", codec.TypeYAML),
)

Custom Codecs

Registering Custom Codecs

import "rivaas.dev/config/codec"

func init() {
    codec.RegisterEncoder("myformat", MyCodec{})
    codec.RegisterDecoder("myformat", MyCodec{})
}

Registration functions:

func RegisterEncoder(name string, encoder Codec)
func RegisterDecoder(name string, decoder Codec)

Custom Codec Example

type MyCodec struct{}

func (c MyCodec) Encode(v any) ([]byte, error) {
    data, ok := v.(map[string]any)
    if !ok {
        return nil, fmt.Errorf("expected map[string]any, got %T", v)
    }
    
    // Your encoding logic
    var buf bytes.Buffer
    // ... write to buf ...
    
    return buf.Bytes(), nil
}

func (c MyCodec) Decode(data []byte, v any) error {
    target, ok := v.(*map[string]any)
    if !ok {
        return fmt.Errorf("expected *map[string]any, got %T", v)
    }
    
    // Your decoding logic
    result := make(map[string]any)
    // ... parse data into result ...
    
    *target = result
    return nil
}

func init() {
    codec.RegisterEncoder("myformat", MyCodec{})
    codec.RegisterDecoder("myformat", MyCodec{})
}

See: Custom Codecs Guide

Common Patterns

Pattern 1: Mixed Formats

cfg := config.MustNew(
    config.WithFile("config.yaml"),  // YAML
    config.WithFile("secrets.json"), // JSON
    config.WithFile("extra.toml"),   // TOML
)

Pattern 2: Explicit Format

cfg := config.MustNew(
    config.WithFileAs("config.txt", codec.TypeYAML),
)

Pattern 3: Content Source

yamlData := []byte(`server: {port: 8080}`)
cfg := config.MustNew(
    config.WithContent(yamlData, codec.TypeYAML),
)

Pattern 4: Custom Codec

import _ "yourmodule/xmlcodec"  // Registers custom codec

cfg := config.MustNew(
    config.WithFileAs("config.xml", "xml"),
)

Type Conversion Examples

String to Duration

timeout: "30s"
timeout := cfg.Duration("timeout")  // 30 * time.Second

String to Int

port: "8080"
port := cfg.Int("port")  // 8080

String to Bool

debug: "true"
debug := cfg.Bool("debug")  // true

String to Time

created: "2025-01-01T00:00:00Z"
created := cfg.Time("created")  // time.Time

Error Handling

Decode Errors

if err := cfg.Load(context.Background()); err != nil {
    // Error format:
    // "config error in source[0] during load: yaml: unmarshal error"
    log.Printf("Failed to decode: %v", err)
}

Encode Errors

if err := cfg.Dump(context.Background()); err != nil {
    // Error format:
    // "config error in dumper[0] during dump: json: unsupported type"
    log.Printf("Failed to encode: %v", err)
}

Type Conversion Errors

// For error-returning methods
port, err := config.GetE[int](cfg, "server.port")
if err != nil {
    log.Printf("Invalid port: %v", err)
}

Performance Notes

  • JSON: Fast, minimal overhead
  • YAML: Moderate overhead (parsing complexity)
  • TOML: Fast, strict typing
  • Casters: Minimal overhead, optimized for common cases

Next Steps

4 - Troubleshooting

Common issues, solutions, and frequently asked questions

Solutions to common problems and frequently asked questions about the config package.

Configuration Loading Issues

File Not Found

Problem: Configuration file cannot be found.

config error in source[0] during load: open config.yaml: no such file or directory

Solutions:

  1. Check file path: Ensure the path is correct relative to where your application runs.
// Use absolute path if needed
cfg := config.MustNew(
    config.WithFile("/absolute/path/to/config.yaml"),
)
  1. Check working directory: Verify your application’s working directory.
wd, _ := os.Getwd()
fmt.Printf("Working directory: %s\n", wd)
  1. Make file optional: Handle missing files gracefully.
cfg, err := config.New(
    config.WithFile("config.yaml"),
)
if err != nil {
    log.Printf("Config file not found, using defaults: %v", err)
    // Use defaults
}

Format Not Recognized

Problem: File extension doesn’t match a known format.

config error in source[0] during load: no decoder registered for extension .conf

Solutions:

  1. Use explicit format:
cfg := config.MustNew(
    config.WithFileAs("config.conf", codec.TypeYAML),
)
  1. Register custom codec:
import _ "yourmodule/mycodec"  // Registers .conf format

Parse Errors

Problem: Configuration file has syntax errors.

config error in source[0] during load: yaml: unmarshal error

Solutions:

  1. Validate YAML/JSON syntax: Use online validators or linters
  2. Check indentation: YAML is indentation-sensitive
  3. Quote strings: Quote values with special characters
# Bad
url: http://example.com:8080

# Good
url: "http://example.com:8080"

Struct Binding Issues

Struct Not Populating

Problem: Struct fields remain at zero values after loading.

Solutions:

  1. Pass pointer to struct:
// Wrong
cfg := config.MustNew(config.WithBinding(myConfig))

// Correct
cfg := config.MustNew(config.WithBinding(&myConfig))
  1. Check struct tags:
// Config file: server.port = 8080
type Config struct {
    // Wrong - doesn't match config structure
    Port int `config:"port"`
    
    // Correct - matches nested structure
    Server struct {
        Port int `config:"port"`
    } `config:"server"`
}
  1. Verify tag names match config keys:
# config.yaml
server:
  host: localhost
  port: 8080
type Config struct {
    Server struct {
        Host string `config:"host"`  // Must match "host" in YAML
        Port int    `config:"port"`  // Must match "port" in YAML
    } `config:"server"`  // Must match "server" in YAML
}
  1. Export struct fields: Fields must be exported (start with uppercase)
// Wrong - unexported fields won't be populated
type Config struct {
    port int `config:"port"`
}

// Correct - exported field
type Config struct {
    Port int `config:"port"`
}

Type Mismatch Errors

Problem: Configuration value type doesn’t match struct field type.

Solutions:

  1. Use compatible types: Ensure types can be converted
# config.yaml
port: "8080"  # String
type Config struct {
    Port int `config:"port"`  // Will be converted from string
}
  1. Check slice vs scalar: Don’t mix slice and scalar values
# Wrong - port is an array but struct expects int
ports:
  - 8080
  - 8081
type Config struct {
    Ports []int `config:"ports"`  // Correct - expects slice
}

Validation Errors

Problem: Struct validation fails.

config error in binding during validate: port must be positive

Solutions:

  1. Check validation logic:
func (c *Config) Validate() error {
    if c.Port <= 0 {
        return fmt.Errorf("port must be positive, got %d", c.Port)
    }
    return nil
}
  1. Provide helpful error messages: Include the actual value in error

  2. Check validation order: Validation runs after binding

Environment Variable Issues

Environment Variables Not Loading

Problem: Environment variables are not being picked up.

Solutions:

  1. Check prefix: Ensure environment variables have the correct prefix
# Wrong - missing prefix
export SERVER_PORT=8080

# Correct - with MYAPP_ prefix
export MYAPP_SERVER_PORT=8080
cfg := config.MustNew(
    config.WithEnv("MYAPP_"),  // Must match prefix
)
  1. Verify environment variables are set:
env | grep MYAPP_
  1. Check variable names: Use underscores for nesting
# Maps to server.port
export MYAPP_SERVER_PORT=8080

# Maps to database.primary.host
export MYAPP_DATABASE_PRIMARY_HOST=localhost

Environment Variable Mapping Issues

Problem: Environment variables aren’t mapping to the right config keys.

Solutions:

  1. Understand naming convention:
Environment VariableConfig Path
MYAPP_SERVER_PORTserver.port
MYAPP_FOO_BAR_BAZfoo.bar.baz
MYAPP_FOO__BARfoo.bar (double underscore)
  1. Check case sensitivity: Environment variables are converted to lowercase
export MYAPP_SERVER_PORT=8080  # Becomes: server.port
  1. Test mapping:
cfg := config.MustNew(
    config.WithEnv("MYAPP_"),
)
cfg.Load(context.Background())

// Print effective configuration
values := cfg.Values()
fmt.Printf("Config: %+v\n", *values)

Type Conflicts

Problem: Environment variable creates conflict between scalar and nested.

export MYAPP_FOO=scalar
export MYAPP_FOO_BAR=nested

Solution: Nested structures take precedence. Result is foo.bar = "nested", scalar foo is overwritten.

Best practice: Don’t create such conflicts; structure your configuration hierarchically.

Validation Issues

Schema Validation Failures

Problem: JSON Schema validation fails.

config error in json-schema during validate: server.port: must be >= 1

Solutions:

  1. Check schema requirements: Ensure configuration meets schema constraints

  2. Debug with schema validator: Use online JSON Schema validators

  3. Provide all required fields:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": ["server", "database"]
}

Custom Validation Errors

Problem: Custom validation function fails.

Solutions:

  1. Add detailed error messages:
config.WithValidator(func(data map[string]any) error {
    port, ok := data["port"].(int)
    if !ok {
        return fmt.Errorf("port must be an integer, got %T", data["port"])
    }
    if port < 1 || port > 65535 {
        return fmt.Errorf("port must be 1-65535, got %d", port)
    }
    return nil
})
  1. Check data types: Values in map might not be expected type
// Type assertion with check
if port, ok := data["port"].(int); ok {
    // Use port
}

Performance Issues

Slow Configuration Loading

Problem: Configuration loading takes too long.

Solutions:

  1. Reduce source count: Combine configuration files when possible

  2. Avoid remote sources in hot paths: Cache remote configuration

  3. Profile loading:

start := time.Now()
err := cfg.Load(context.Background())
log.Printf("Config load time: %v", time.Since(start))
  1. Load once: Load configuration during initialization, not per-request

Memory Usage

Problem: High memory usage.

Solutions:

  1. Don’t keep multiple Config instances: Reuse single instance

  2. Clear unnecessary dumpers: Only use dumpers when needed

// Development only
if debug {
    cfg = config.MustNew(
        config.WithFile("config.yaml"),
        config.WithFileDumper("debug-config.yaml"),
    )
}

Common Misconceptions

Q: Why don’t changes to config files take effect?

A: Configuration is loaded once during Load(). It’s not automatically reloaded when files change.

Solution: Reload configuration explicitly:

// Reload configuration
if err := cfg.Load(context.Background()); err != nil {
    log.Printf("Failed to reload: %v", err)
}

Q: Why does my config work locally but not in Docker?

A: Likely a path or working directory issue.

Solutions:

  1. Use absolute paths in Docker:
cfg := config.MustNew(
    config.WithFile("/app/config/config.yaml"),
)
  1. Set working directory in Dockerfile:
WORKDIR /app
COPY config.yaml .
  1. Use environment variables for container configuration:
cfg := config.MustNew(
    config.WithFile("config.yaml"),     // Defaults
    config.WithEnv("APP_"),             // Override in container
)

Q: Can I modify configuration at runtime?

A: The Config instance is read-only after loading. You need to reload to pick up changes.

Pattern for dynamic updates:

type ConfigManager struct {
    cfg *config.Config
    mu  sync.RWMutex
}

func (cm *ConfigManager) Reload(ctx context.Context) error {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    return cm.cfg.Load(ctx)
}

func (cm *ConfigManager) Get(key string) any {
    cm.mu.RLock()
    defer cm.mu.RUnlock()
    return cm.cfg.Get(key)
}

FAQ

Q: Is Config thread-safe?

A: Yes, Load() and all getter methods are thread-safe.

Q: What happens with nil Config instances?

A: Getter methods return zero values, error methods return errors. No panics.

Q: Can I load from multiple sources?

A: Yes, sources are merged with later sources overriding earlier ones.

Q: How do I handle secrets?

A:

  1. Use environment variables for secrets (not config files)
  2. Use secret management systems (Vault, AWS Secrets Manager)
  3. Never commit secrets to version control

Q: Can I use the same struct tags for JSON and config?

A: Yes, using WithTag():

type Config struct {
    Port int `json:"port"`
}

cfg := config.MustNew(
    config.WithTag("json"),
    config.WithBinding(&myConfig),
)

Q: How do I debug configuration loading?

A:

  1. Use WithFileDumper() to see merged config
  2. Print values after loading
  3. Check error messages for source context
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
    config.WithFileDumper("debug-config.yaml"),
)

if err := cfg.Load(context.Background()); err != nil {
    log.Printf("Load error: %v", err)
}

cfg.Dump(context.Background())  // Writes to debug-config.yaml

values := cfg.Values()
fmt.Printf("Loaded config: %+v\n", *values)

Q: What’s the difference between Get, GetE, and GetOr?

A:

  • Get[T]() - Returns value or zero value (no error)
  • GetE[T]() - Returns value and error
  • GetOr[T]() - Returns value or provided default

Q: Can I use config without struct binding?

A: Yes, use getter methods directly:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
)
cfg.Load(context.Background())

port := cfg.Int("server.port")
host := cfg.String("server.host")

Q: How do I validate required fields?

A: Use struct validation:

func (c *Config) Validate() error {
    if c.Database.Host == "" {
        return errors.New("database.host is required")
    }
    return nil
}

Performance Notes

Configuration access:

  • Getter methods: O(n) where n = dot notation depth
  • Direct Get(): O(n)
  • No caching of individual keys

Best practices:

  • Load configuration once at startup
  • Cache frequently accessed values in local variables
  • Use struct binding for best performance

Thread safety overhead:

  • Minimal locking overhead
  • Read operations are concurrent
  • Write operations (Load) use exclusive lock

Getting Help

If you encounter issues not covered here:

  1. Check the Configuration Guide
  2. Review API Reference
  3. Search GitHub Issues
  4. Ask in the community forums

When reporting issues, include:

  • Go version
  • Config package version
  • Minimal reproducible example
  • Error messages
  • Expected vs actual behavior