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 loads settings from files, environment variables, and Consul, merges them in order, and supports validation and struct binding.

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 typed getters (String, Int, Get, and similar); do not call Values() on a nil *Config

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(opts...)     // With error handling
cfg := config.MustNew(opts...)      // Panics on error

Options must not be nil; passing a nil option results in a validation error (reported by New, panic by MustNew). This rule applies to all Rivaas packages that use functional options.

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

All option functions for New and MustNew:

  • Source options (WithFile, WithEnv, WithConsul, WithConsulOptional, 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.

Error

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

Configuration errors use the exported type config.Error (some comments refer to this shape as “config error”). Use errors.As with *config.Error to inspect Source, Operation, and Err.

Option

Option is a functional option for New / MustNew. It applies to an internal builder type, not *Config directly—pass only values returned from WithFile, WithEnv, and other With… functions. Validation errors from options are collected when the config is built.

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 typed getters (Get, String, Int, and similar) on a nil *Config return zero values. Do not call Values() on a nil receiver (it will panic).
  • Hierarchical data storage with dot notation support.

Error

type Error 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
}

Exported error type (config.Error) with context for failed loads, validation, or binding. For field-level context, the package also provides NewFieldError. Unwrap with var e *config.Error and errors.As(err, &e).

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 (nil on error).
  • error - Error if initialization fails.

On error, New returns (nil, err); do not use the config when an error is returned.

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 validation fails after applying options

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 - *config.Error (or wrapped) 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).

MustLoad

func (c *Config) MustLoad(ctx context.Context)

Calls Load and panics if loading fails. Use when failure should stop the program (for example in main).

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.

MustDump

func (c *Config) MustDump(ctx context.Context)

Calls Dump and panics if dumping fails.

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. Supports built-in types handled by the same conversion path as spf13/cast (see get.go). It does not decode a nested map[string]any into a custom struct; use WithBinding for struct-shaped config.

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, conversion fails, or nil instance

Example:

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

// Nested objects are map[string]any until you bind to a struct
db, err := config.GetE[map[string]any](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 loaded configuration map after Load. If no load has happened yet (c.values is nil), it returns a pointer to a new empty map (not shared with future loads).

Nil receiver: Do not call on a nil *Config (panics).

Warning: Treat the map as read-only; mutating it bypasses normal locking.

Example:

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

Nil-Safety Guarantees

Typed getters and Get handle a nil *Config without panicking:

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"

Avoid cfg.Values() when cfg may be nil; it is not nil-receiver-safe.

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 cfgErr *config.Error
    if errors.As(err, &cfgErr) {
        log.Printf("Config error in %s during %s: %v",
            cfgErr.Source, cfgErr.Operation, cfgErr.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

Reference for all option functions passed to New and MustNew.

Option Type

Option is a functional option that applies to an internal builder during construction, then produces the public *Config. It is not func(*Config). Pass only non-nil values from WithFile, WithEnv, and other With… helpers. Validation errors from options are collected when you call New (or when MustNew panics).

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.

CONSUL_HTTP_ADDR is required. If it is not set, New/MustNew returns a validation error at construction. For optional Consul (e.g. development without Consul), use WithConsulOptional instead.

Parameters:

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

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithConsul("production/service.json"),  // Fails at construction if CONSUL_HTTP_ADDR is unset
)

Environment variables:

  • CONSUL_HTTP_ADDR - Consul server address (required)
  • 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.

CONSUL_HTTP_ADDR is required. If it is not set, New/MustNew returns a validation error at construction. For optional Consul, use WithConsulAsOptional instead.

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)
  • CONSUL_HTTP_TOKEN - Access token for authentication (optional)

WithConsulOptional

func WithConsulOptional(path string) Option

Adds a Consul source only when CONSUL_HTTP_ADDR is set. If it is not set, this option is a no-op (no source added, no error). Use for development without Consul; use WithConsul when Consul is required and should fail at construction if the env is missing.

Parameters:

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

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithConsulOptional("production/service.yaml"),  // No-op when CONSUL_HTTP_ADDR is unset
)

WithConsulAsOptional

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

Adds a Consul source with explicit format only when CONSUL_HTTP_ADDR is set. If it is not set, this option is a no-op. Use for development without Consul; use WithConsulAs when Consul is required.

Parameters:

  • path - Consul key path
  • codecType - Codec type (e.g. codec.TypeYAML, codec.TypeJSON)

Example:

cfg := config.MustNew(
    config.WithConsulAsOptional("production/service", codec.TypeJSON),
)

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 AppSettings struct {
    Port int `config:"port"`
}

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

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 AppSettings struct {
    Port int `yaml:"port"`
}

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

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: Runs after sources are merged and after JSON Schema validation (if any), 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, values *map[string]any) error
}

Example:

type CustomDumper struct{}

func (d *CustomDumper) Dump(ctx context.Context, values *map[string]any) error {
    // Write *values somewhere; do not mutate the map unless you own the contract
    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.

Encoder and Decoder

The rivaas.dev/config/codec package registers implementations by name (codec.Type). Two interfaces are used:

type Encoder interface {
    Encode(v any) ([]byte, error)
}

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

Format codecs (JSON, YAML, TOML) implement both and are registered for Encode and Decode. Caster types are registered as decoders only (see below).

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:

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 are decoder registrations in the codec registry (RegisterDecoder only). They are useful when you plug decoders into custom pipelines.

Config getters (String, Int, Bool, Duration, and so on) use github.com/spf13/cast on merged values—they do not call codec.GetDecoder(TypeCaster…) for each read.

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")

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")

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-
Caster types (Bool, Int*, …)-

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(codec.Type("myformat"), MyCodec{})
    codec.RegisterDecoder(codec.Type("myformat"), MyCodec{})
}

Registration functions:

func RegisterEncoder(name codec.Type, encoder codec.Encoder)
func RegisterDecoder(name codec.Type, decoder codec.Decoder)

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(codec.Type("myformat"), MyCodec{})
    codec.RegisterDecoder(codec.Type("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 (
    "rivaas.dev/config"
    "rivaas.dev/config/codec"
    _ "yourmodule/xmlcodec" // registers custom codec in init()
)

cfg := config.MustNew(
    config.WithFileAs("config.xml", codec.Type("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
  • Registry caster decoders: small overhead when used in custom decode paths; getters use spf13/cast separately

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