config

package
v3.0.0-next.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 4 Imported by: 0

README

Configuration Management

Go Reference

Type-safe YAML configuration with environment variable overrides using Viper and Go generics.

Overview

The config package provides a simple, type-safe way to load configuration from YAML strings with automatic environment variable support. Built on top of Viper, it leverages Go generics for compile-time type safety.

Features

  • Type-Safe: Generic functions ensure compile-time type checking
  • Environment Overrides: Automatic environment variable support with configurable prefix
  • Functional Options: LoadStringWithOptions accepts Option values (WithEnvPrefix, WithDefaults, WithNestedEnvVars) for advanced configuration
  • Nested Configuration: Map environment variables onto map-typed config sections with WithNestedEnvVars
  • Simple API: Load configuration in one function call

Installation

go get github.com/jasoet/pkg/v3/config

Quick Start

Compile-checked versions of these snippets live in example_test.go; a runnable end-to-end program lives in examples/config/.

Basic Usage
package main

import (
    "fmt"

    "github.com/jasoet/pkg/v3/config"
)

type AppConfig struct {
    Name    string `yaml:"name"`
    Version string `yaml:"version"`
    Server  struct {
        Host string `yaml:"host"`
        Port int    `yaml:"port"`
    } `yaml:"server"`
}

func main() {
    yamlConfig := `
name: my-app
version: 1.0.0
server:
  host: localhost
  port: 8080
`

    cfg, err := config.LoadString[AppConfig](yamlConfig)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%s v%s on %s:%d\n",
        cfg.Name, cfg.Version, cfg.Server.Host, cfg.Server.Port)
}
Environment Variable Overrides

By default, environment variables with the ENV_ prefix override YAML values. Dots in nested keys become underscores (server.portENV_SERVER_PORT):

os.Setenv("ENV_SERVER_PORT", "9090")

cfg, err := config.LoadString[AppConfig](yamlConfig)
// cfg.Server.Port == 9090 (from env), other fields from YAML
Custom Environment Prefix

Pass a prefix as the second argument to LoadString (only the first value is used; additional values are ignored):

cfg, err := config.LoadString[AppConfig](yamlConfig, "MYAPP")
// Now MYAPP_* environment variables apply, e.g. MYAPP_SERVER_PORT=9090

Options API

LoadStringWithOptions applies functional options after the YAML has been parsed and before unmarshaling:

func LoadStringWithOptions[T any](configString string, opts ...Option) (*T, error)

An Option is a func(*viper.Viper), so besides the provided constructors you can pass any custom function that mutates the underlying Viper instance (see examples/config/ for a custom-option example).

WithDefaults

Sets default values for keys absent from the YAML:

cfg, err := config.LoadStringWithOptions[AppConfig](`server: {port: 8080}`,
    config.WithDefaults(map[string]any{"debug": true}),
    config.WithEnvPrefix("APP"),
)
// cfg.Debug == true (default), cfg.Server.Port == 8080 (YAML),
// or 9090 if APP_SERVER_PORT=9090 is set (env override)
WithNestedEnvVars

Maps prefixed environment variables onto a map-typed config section:

func WithNestedEnvVars(prefix string, keyDepth int, configPath string) Option
  • prefix: prefix of the environment variables to process (e.g. "APP").
  • keyDepth: prefix-relative — the prefix is stripped first, then keyDepth indexes the remaining underscore-split tokens to locate the entity name; everything after it forms the field name.
  • configPath: base path in the configuration where values are set.
type Config struct {
    Users map[string]map[string]string `yaml:"users"`
}

// APP_USERS_ADMIN_NAME: strip "APP" -> ["USERS", "ADMIN", "NAME"];
// keyDepth 1 -> entity "admin", field "name" under path "users".
os.Setenv("APP_USERS_ADMIN_NAME", "alice")

cfg, err := config.LoadStringWithOptions[Config](``,
    config.WithNestedEnvVars("APP", 1, "users"),
)
// cfg.Users["admin"]["name"] == "alice"

Precedence contract: nested env vars fill only keys that are absent from the YAML. If the YAML already sets a key, the environment variable is ignored:

os.Setenv("APP_USERS_ADMIN_NAME", "alice")
os.Setenv("APP_USERS_ADMIN_EMAIL", "alice@example.com")

cfg, _ := config.LoadStringWithOptions[Config](`users: {admin: {name: bob}}`,
    config.WithNestedEnvVars("APP", 1, "users"),
)
// cfg.Users["admin"]["name"]  == "bob"              (YAML wins)
// cfg.Users["admin"]["email"] == "alice@example.com" (filled from env)

Note: unlike the flat ENV_ override mechanism (which overrides YAML), WithNestedEnvVars never overrides YAML keys.

Migrating from v2 NestedEnvVars: keyDepth is now prefix-relative — subtract the number of prefix tokens from your old keyDepth value (e.g. old 2 with prefix "MY_APP_" becomes 1; old 1 with prefix "APP" becomes 0).

Struct Tags

Decoding is case-insensitive via mapstructure, so plain yaml tags (as used throughout these examples) are sufficient. Adding matching mapstructure tags is harmless but not required.

Testing

go test ./config/ -v

The package's tests use t.Setenv to isolate environment variable fixtures.

Examples

See examples/config/ for a runnable program covering:

  • Basic configuration loading
  • Environment variable overrides
  • Custom environment prefix
  • Custom Option functions
  • Nested environment variables with WithNestedEnvVars
  • otel - OpenTelemetry configuration
  • db - Database configuration
  • server - HTTP server configuration
  • grpc - gRPC server configuration

License

MIT License - see LICENSE for details.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func LoadString

func LoadString[T any](configString string, envPrefix ...string) (*T, error)

LoadString loads configuration from a string with optional environment variable support. Parameters:

  • configString: The configuration string in YAML format
  • envPrefix: Optional environment variable prefix (default: "ENV"). Only the first value is used; any additional values are ignored.
Example

Basic loading from a YAML string, plus the default ENV_ override mechanism. Examples have no *testing.T, so environment variables are managed with os.Setenv/os.Unsetenv directly.

package main

import (
	"fmt"
	"os"

	"github.com/jasoet/pkg/v3/config"
)

func main() {
	type AppConfig struct {
		Name    string `yaml:"name"`
		Version string `yaml:"version"`
		Server  struct {
			Host string `yaml:"host"`
			Port int    `yaml:"port"`
		} `yaml:"server"`
	}

	yamlConfig := `
name: my-app
version: 1.0.0
server:
  host: localhost
  port: 8080
`

	// Dots become underscores: server.port is overridden by ENV_SERVER_PORT.
	os.Setenv("ENV_SERVER_PORT", "9090")
	defer os.Unsetenv("ENV_SERVER_PORT")

	cfg, err := config.LoadString[AppConfig](yamlConfig)
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Printf("%s v%s on %s:%d\n", cfg.Name, cfg.Version, cfg.Server.Host, cfg.Server.Port)

}
Output:
my-app v1.0.0 on localhost:9090

func LoadStringWithOptions

func LoadStringWithOptions[T any](configString string, opts ...Option) (*T, error)

LoadStringWithOptions loads configuration from a string with optional environment variable support and applies the given options before unmarshaling. Options run after the YAML has been parsed; options that must precede parsing are not supported. Parameters:

  • configString: The configuration string in YAML format
  • opts: Options to customize the viper configuration before unmarshaling
Example

Loading with functional options: defaults for missing keys and a custom environment variable prefix.

package main

import (
	"fmt"
	"os"

	"github.com/jasoet/pkg/v3/config"
)

func main() {
	type AppConfig struct {
		Debug  bool `yaml:"debug"`
		Server struct {
			Port int `yaml:"port"`
		} `yaml:"server"`
	}

	os.Setenv("APP_SERVER_PORT", "9090")
	defer os.Unsetenv("APP_SERVER_PORT")

	cfg, err := config.LoadStringWithOptions[AppConfig](`server: {port: 8080}`,
		config.WithDefaults(map[string]any{"debug": true}),
		config.WithEnvPrefix("APP"),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Printf("debug=%v port=%d\n", cfg.Debug, cfg.Server.Port)

}
Output:
debug=true port=9090

Types

type Option

type Option func(*viper.Viper)

Option customizes the viper configuration used during loading. Consumers use the provided With* constructors and never need to name viper.

func WithDefaults

func WithDefaults(defaults map[string]any) Option

WithDefaults sets default values for the given keys.

func WithEnvPrefix

func WithEnvPrefix(prefix string) Option

WithEnvPrefix sets the environment variable prefix used for lookups.

func WithNestedEnvVars

func WithNestedEnvVars(prefix string, keyDepth int, configPath string) Option

WithNestedEnvVars processes environment variables with the given prefix and sets them under configPath, filling only keys absent from the loaded configuration. Parameters:

  • prefix: The prefix of environment variables to process (e.g. "MY_APP_").
  • keyDepth: The zero-based index into the underscore-split key after the prefix has been removed, at which the entity name token is located. For example, given the env var MY_APP_USER_NAME with prefix "MY_APP_", the remaining parts are ["USER", "NAME"]. With keyDepth=0, "USER" is treated as the entity name and "NAME" becomes the field name.
  • configPath: The base path in the configuration where values should be set.
Example

WithNestedEnvVars maps prefixed environment variables onto a map-typed config section. The prefix is stripped first, then keyDepth indexes the remaining underscore-split tokens: with prefix "APP" and keyDepth 1, APP_USERS_ADMIN_NAME yields entity "admin" with field "name" under the "users" config path.

Precedence contract: nested env vars fill only keys absent from the YAML. Here users.admin.name comes from YAML, so APP_USERS_ADMIN_NAME is ignored, while users.admin.email is YAML-absent and filled from the environment.

package main

import (
	"fmt"
	"os"

	"github.com/jasoet/pkg/v3/config"
)

func main() {
	type Config struct {
		Users map[string]map[string]string `yaml:"users"`
	}

	os.Setenv("APP_USERS_ADMIN_NAME", "alice")
	os.Setenv("APP_USERS_ADMIN_EMAIL", "alice@example.com")
	defer os.Unsetenv("APP_USERS_ADMIN_NAME")
	defer os.Unsetenv("APP_USERS_ADMIN_EMAIL")

	cfg, err := config.LoadStringWithOptions[Config](`users: {admin: {name: bob}}`,
		config.WithNestedEnvVars("APP", 1, "users"),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Println(cfg.Users["admin"]["name"])
	fmt.Println(cfg.Users["admin"]["email"])

}
Output:
bob
alice@example.com

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL