Help us shape the future of Go! Take the 2026 Go Developer Survey

advanced

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 26 Imported by: 0

README

typedcfg advanced

Optional multi-source configuration module for Go 1.26.0+: load ordered sources, apply defaults, validate, and read a snapshot.

package main

import (
    "fmt"

    typedcfg "github.com/nextpkg/typedcfg/advanced"
)

type Config struct {
    Port int `json:"port" default:"8080" validate:"min=1,max=65535"`
    MaxConns int `json:"max_conns" default:"10"`
}

func main() {
    cfg := typedcfg.MustLoad[Config]("config.yaml").Get()
    fmt.Println(cfg.Port)
}

MustLoad panics if a file cannot be read, parsing fails, or validation fails, so invalid configuration stops startup immediately. For one-time loading it neither starts a watcher nor retains open files, and no Close is needed. Use Load when the caller needs to handle errors instead of panicking; both methods accept no files to load defaults and run validation.

For multiple sources at startup, NewBuilder[Config]().AddFile("config.yaml").AddEnv("APP_").MustBuild().Get() provides the same panic-on-error behavior. Keep the manager and call Close (or cancel its Build context) when using WithWatch.

Sources and priority

cm, err := typedcfg.NewBuilder[Config]().
    AddFile("base.yaml").
    AddFile("local.json").
    AddEnv("APP_").
    Build(ctx)

Later sources override earlier sources. Supported files: .json, .yaml, .yml; other extensions return an error. TOML and other formats require an explicit external parser through AddProvider.

Use json field tags for all source formats, including YAML. To use existing koanf or yaml tags, select one tag with WithTag("koanf") or WithTag("yaml"); tags are not guessed or combined.

Environment variables use __ for nesting and retain single underscores: APP_DATABASE__MAX_CONNS=25 maps to database.max_conns. Values use koanf's type conversion, including duration strings.

Custom providers explicitly declare their parser:

builder.AddProvider(rawbytes.Provider([]byte(`{"port":9090}`)), json.Parser())
// A provider returning a map from Read() requires no parser:
builder.AddProvider(mapProvider, nil)

Here rawbytes and json are github.com/knadh/koanf/providers/rawbytes and github.com/knadh/koanf/parsers/json. Providers must bound their own I/O and own any resources they allocate; typedcfg does not close them. Providers implementing WatchProvider can be subscribed with WithWatch().

HTTP/HTTPS sources

cm := typedcfg.NewBuilder[Config]().
    AddURL("https://example.com/config.yaml").
    AddEnv("APP_").
    MustBuild()
cfg := cm.Get()

AddURL fetches the URL with GET during Build and each Reload(). Later sources override it in the same way as local files. JSON/YAML parsing is selected from the URL path extension, ignoring query parameters; for endpoints without a supported extension, supply a parser explicitly.

builder.AddURL("https://example.com/config", typedcfg.URLOptions{
    Parser:   json.Parser(),
    Timeout:  5 * time.Second,
    MaxBytes: 2 << 20,
    Headers:  http.Header{"Authorization": []string{"Bearer " + token}},
})

Here json is github.com/knadh/koanf/parsers/json, with time and net/http from the standard library. Defaults are a 10-second timeout and a 1 MiB response-body limit. Client accepts a custom *http.Client for transport/TLS settings; typedcfg copies the client and headers, but a supplied transport remains shared and caller-owned. A shorter Client timeout still applies.

Only 2xx responses are decoded. HTTP errors, oversized responses, parsing failures, and validation failures return an error (or panic through MustBuild); a failed Reload keeps the previous snapshot. Requests are canceled when the Build context is canceled or the manager is closed. Redirects are limited to ten visited requests and must retain the original scheme and host, including port. Use headers for credentials; URL userinfo and fragments are rejected.

Enable automatic HTTP updates with AddURL(address).WithWatch().WithPollInterval(30 * time.Second). The interval defaults to 30 seconds and must be positive when specified. Each tick performs a full GET and reloads all sources; requests are serialized, with no overlapping polls. Failed fetches or invalid configurations preserve the last valid snapshot, report through Errors(), and are retried on the next tick. This is periodic polling, not server push; ETag/Last-Modified conditional requests are not used. Without WithWatch, URLs are fetched only at Build and explicit Reload(). WithCreateIfMissing() only creates local files; there is no disk cache or remote-file creation.

Alibaba Cloud MSE Nacos

Install the optional module github.com/nextpkg/typedcfg/adapters/nacos:

cm := nacos.MustLoad[Config]("127.0.0.1:8848", "app.yaml")
defer cm.Close()
cfg := cm.Get()

This creates the SDK client, loads and validates the configuration, and enables automatic updates. Startup errors panic; use nacos.Load[Config](ctx, address, dataID, options...) to handle errors. cm.Close() stops updates, closes the SDK client, and removes its temporary cache. Canceling the Load context also triggers cleanup; call Close to wait for it. Get, Reload, and Errors behave like the core manager. The core module does not import the Nacos SDK.

Address is a hostname or IP with an optional port (default 8848); gRPC uses that port plus 1000. DataId extensions .json, .yaml, .yml select the parser; a DataId without an extension defaults to YAML. Other formats require Options.Parser. The default namespace is public and group is DEFAULT_GROUP. Environment variables are read once at startup:

Variables Purpose
ALIBABA_CLOUD_ACCESS_KEY_ID, ALIBABA_CLOUD_ACCESS_KEY_SECRET MSE RAM credentials; both must be set together
NACOS_USERNAME, NACOS_PASSWORD Nacos username authentication; both must be set together
NACOS_NAMESPACE_ID, NACOS_GROUP Optional namespace ID and group

For MSE, supply the instance connection hostname and credentials authorized for the target namespace/configuration; ensure its network whitelist allows SDK connections. Environment values are literal; ${VAR} expansion and automatic credential rotation are not performed.

Optional settings keep the common path short:

cm := nacos.MustLoad[Config]("127.0.0.1:8848", "app.yaml", nacos.Options{
    Group: "MY_GROUP",        // Nonempty explicit values override environment defaults.
    EnvPrefix: "APP_",        // Optional business overrides, such as APP_PORT=9090.
    PollInterval: 5 * time.Second,
})
defer cm.Close()

The default SDK timeout is 5 seconds, with subscriptions and 30-second periodic refresh to recover from missed notifications. Timeout, Parser, NamespaceID and LogDir can also be set in Options. Each Load uses a fresh private cache and disables stale snapshot fallback. SDK logs remain under the user's cache directory at typedcfg/nacos/log by default (10 MiB rotation, one backup, seven-day retention). The SDK uses a process-global logger; applications needing full SDK control should use Provider instead. See the complete watching example: set NACOS_ADDRESS and NACOS_DATA_ID, run it, change the remote configuration, and observe the printed port without restarting.

For existing SDK clients or advanced connection settings, retain explicit ownership:

cm := typedcfg.NewBuilder[Config]().
    AddProvider(nacos.Provider(client, "app.yaml", "DEFAULT_GROUP"), yaml.Parser()).
    WithWatch().MustBuild()
defer cm.Close() // Stop the manager before calling client.CloseClient().

Here yaml is github.com/knadh/koanf/parsers/yaml; use json.Parser() for JSON.

The adapter pins SDK v2.2.8 for static AccessKey authentication: v2.3.5 has a data race between RAM credential refresh and request signing. Keep race testing enabled when upgrading the SDK; applications can override module versions, so the version selected by their dependency graph also needs verification.

With Provider, Build and Reload call the SDK's GetConfig. Add .WithWatch() to the builder to subscribe through the SDK's ListenConfig; notifications automatically re-read all sources, validate, and publish a new snapshot. No application Reload() call is needed. Close() or Build-context cancellation removes subscriptions via CancelListenConfig, but does not close the caller-owned SDK client. The adapter rejects a second subscription for the same client pointer, DataId and Group with nacos.ErrAlreadyWatched, even through different Provider objects; a failed Build releases its reservation, and successful cancellation allows a new manager to subscribe. A failed cancellation retains the reservation to prevent overlapping ownership. Watched clients must be pointers (as the SDK client is). Do not also call the SDK's ListenConfig/CancelListenConfig for that key directly, or wrap one SDK client in multiple identities: external subscriptions are outside this adapter's ownership checks. Watch setup errors fail Build; failed reloads report through Errors() and preserve the last valid snapshot. Subscribed sources are also re-read every 30 seconds (adjust with WithPollInterval) to recover from missed notifications. SDK v2.2.8 can omit callbacks on deletion or recreation; periodic refresh reports missing content and restores automatic updates after valid content is published again. There is no remote creation. Empty/missing content is an error; publish {} explicitly to use defaults only. Read, parse, or validation errors preserve the previous snapshot. The SDK can fall back to local cache unless DisableUseSnapShot is enabled; NotLoadCacheAtStart alone does not disable that fallback. Existing SDK failover files may also override remote data. Load disables snapshot fallback and uses a fresh temporary cache directory.

When using Provider, the application owns the SDK client: configure TimeoutMs and call client.CloseClient() after the manager is no longer used. SDK reads do not accept a context, so canceling Build or closing the manager cannot interrupt an in-flight SDK call; it must return before Close completes. SDK authentication, cache, logging, and connection policies remain client responsibilities.

The opt-in integration test publishes and removes a unique test configuration on a disposable Nacos instance; its gRPC port must be the HTTP port plus 1000. It defaults to localhost:

cd adapters/nacos
TYPEDCFG_NACOS_TEST_PORT=18848 go test -race . -run TestServerIntegration -count=1

For a dedicated MSE test instance, also set TYPEDCFG_NACOS_TEST_HOST, optional TYPEDCFG_NACOS_TEST_NAMESPACE, and the AccessKey environment variables above. The test credentials need read/write/delete access to group TYPEDCFG_TEST. The test waits up to 15 seconds for each published value to become readable before checking the provider, and removes its unique DataId during cleanup.

Create a missing configuration file

Use WithCreateIfMissing() to initialize missing files from the fixed default tags in your configuration struct:

cm := typedcfg.NewBuilder[Config]().
    AddFile("config.yaml").
    WithCreateIfMissing().
    MustBuild()
cfg := cm.Get()

Creation is opt-in and happens once during Build, for each missing AddFile path. JSON/YAML output uses the selected decoding tag (json by default), includes nested defaults, and creates missing parent directories. Each missing file contains the fields serialized from a default-initialized struct using the selected tags; when layering files, those generated values participate in the normal source order. Existing files, including invalid files, are never replaced. Environment and CLI overrides are only merged in memory and never written into the generated file.

Files are written with owner-only permissions (0600), in newly created directories with 0700 permissions, subject to the platform's permission semantics. Creation requires hard-link support on the target filesystem. Concurrent initializers do not overwrite existing files or expose partially written content. Publication and temporary-file cleanup errors are returned to the caller.

Normal loading and validation follow creation; if validation fails (for example, a required setting has no default), the generated template remains for you to edit. MustBuild panics on creation/loading/validation errors. Load and MustLoad return an error or panic for missing files; automatic creation is available through Builder.WithCreateIfMissing(). Deleting a watched file after startup reports an error and preserves the last snapshot; it does not regenerate it.

Defaults and validation

Defaults are applied before decoding, so explicit false, 0, and empty strings override defaults. Missing keys retain defaults. default tags support primitive types, durations, string slices, and nested structs; see defaults for pointer behavior.

Validation uses validate tags from go-playground/validator. An optional Validate() error method on the config pointer runs after tag validation. T must be a struct; unsupported configuration types return an error.

Reload and optional watching

WithWatch() enables file watching, HTTP polling, and subscriptions for WatchProvider sources; Load/MustLoad alone do not enable watching. Keep the manager and call cm.Get() for each read of the current configuration: a previously saved cfg := cm.Get() remains the old snapshot and is not mutated in place.

cm.Reload() builds a fresh merged state, applies defaults, validates, and atomically publishes it. Deleted keys disappear or return to their defaults. A parse/read/validation failure keeps the previous snapshot. Both manual and automatic reloads are serialized.

cm, err := typedcfg.NewBuilder[Config]().
    AddFile("config.yaml").
    WithCreateIfMissing().
    WithWatch().
    Build(ctx)
if err != nil { return err }
defer func() {
    if err := cm.Close(); err != nil { log.Printf("close configuration: %v", err) }
}()

for err := range cm.Errors() {
    log.Printf("configuration reload failed: %v", err)
}

WithWatch requires at least one AddFile, AddURL, or WatchProvider source. Watch setup failures are returned by Build. Parent directories are watched so atomic replacement of a target file works; unrelated filenames are ignored and target events are debounced for 50 ms. Missing/invalid files retain the previous snapshot and produce an error; restoring a valid file resumes updates. Watching starts after any requested default-file creation and before the initial source read, so subsequent file events and provider notifications are queued during that read.

Errors() buffers one pending asynchronous error; additional errors may be dropped until consumed. Canceling the Build context stops watching and closes the channel; Close also stops and joins the watcher. Without watching, the channel closes on Close. File failures retry on the next event or explicit Reload; remote sources also retry on each periodic refresh.

Get() returns the shared read-only snapshot through an atomic pointer load, without copying or allocating. Do not modify any field, map, slice, pointer target, or interface-held data reachable from it: Go does not enforce read-only pointers, and writes can corrupt configuration and race with other readers. Reload publishes a new object and does not mutate old snapshots. Fetch once per request/operation so related fields use the same version.

Use cfg := cm.Snapshot() when you need to modify configuration locally: it returns an independent deep copy, including nested maps, slices, pointers, arrays, and interface values. Modifying that copy does not affect the manager or other readers. Sharing the same mutable copy between goroutines still requires application synchronization.

current := cm.Get()       // Shared snapshot: read only.
local := cm.Snapshot()    // Private copy: safe to edit locally.
local.Port = 9090         // Does not change current.Port or the manager.

Explicit Snapshot calls allocate and cost time proportional to the configuration data. Snapshot publication also copies the decoded value to detach retained provider/validator references. Configuration is data: channels, functions, unsafe pointers, synchronization primitives, and unexported fields holding non-nil mutable references cannot be safely isolated and cause Build/Reload to fail, preserving any previous snapshot. time.Time is supported as a value with an immutable location. Custom providers and validators must not mutate data concurrently while loading/copying is in progress. Close is idempotent and safe concurrently with reads/reloads; it prevents further reloads but preserves the last snapshot. A custom provider read already in progress must finish before Close returns.

Optional CLI adapter

The core package does not import urfave/cli. Inside a parsed cli.Command.Action, select flags explicitly:

builder.AddProvider(cliv3.Provider(cmd, "server.port", "enabled"), nil)

Import github.com/nextpkg/typedcfg/advanced/adapters/cliv3. Selected flags override configuration when urfave/cli reports them as set (cmd.IsSet), including values from its configured environment/file sources or programmatic Set calls. A flag's default Value alone does not override configuration; explicitly set false/zero values do. Command names are never prepended. Use canonical flag names in the selection, even if the invocation uses aliases. Unselected flags such as --config stay outside the config map. See the executable CLI example.

Examples and development

Core examples share the root module and compile with the library:

go run ./example/basic
go run ./example/cli --server.port 9090 --enabled=false
go run ./example/watch example/watch/config.yaml
# Or provide a new path; the watch example initializes it from defaults.
go test ./...
go test -race ./...
go vet ./...
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 run ./...

The optional Nacos module has its own dependencies; run the same test, vet, and lint commands inside adapters/nacos as well. To test it against local core changes, run go mod edit -replace github.com/nextpkg/typedcfg=../.. inside that directory, then remove the local override with go mod edit -dropreplace github.com/nextpkg/typedcfg before committing.

Documentation

Overview

Package typedcfg loads ordered configuration sources into validated, typed snapshots.

Index

Constants

View Source
const (
	ErrorTypeUnknown           = typedcfg.ErrorTypeUnknown
	ErrorTypeFileNotFound      = typedcfg.ErrorTypeFileNotFound
	ErrorTypeParseFailure      = typedcfg.ErrorTypeParseFailure
	ErrorTypeValidationFailure = typedcfg.ErrorTypeValidationFailure
	ErrorTypeWatchFailure      = typedcfg.ErrorTypeWatchFailure
	ErrorTypeMergeFailure      = typedcfg.ErrorTypeMergeFailure
)

Variables

View Source
var ErrClosed = typedcfg.ErrClosed
View Source
var NewConfigError = typedcfg.NewConfigError
View Source
var NewParseError = typedcfg.NewParseError
View Source
var NewValidationError = typedcfg.NewValidationError

Functions

This section is empty.

Types

type Builder

type Builder[T any] struct {
	// contains filtered or unexported fields
}

Builder configures ordered sources. Later sources override earlier ones. Configure it before Build; concurrent mutation is not supported.

func NewBuilder

func NewBuilder[T any]() *Builder[T]

func (*Builder[T]) AddEnv

func (b *Builder[T]) AddEnv(prefix string) *Builder[T]

AddEnv strips prefix, lowercases keys, and maps double underscores to dots. APP_DATABASE__MAX_CONNS maps to database.max_conns; single underscores remain literal.

func (*Builder[T]) AddFile

func (b *Builder[T]) AddFile(path string) *Builder[T]

AddFile adds a JSON or YAML file. Unsupported extensions fail at Build.

func (*Builder[T]) AddProvider

func (b *Builder[T]) AddProvider(provider koanf.Provider, parser koanf.Parser) *Builder[T]

AddProvider pairs a provider with an explicit parser; nil uses provider.Read(). WithWatch subscribes to providers implementing WatchProvider.

func (*Builder[T]) AddURL

func (b *Builder[T]) AddURL(address string, options ...URLOptions) *Builder[T]

AddURL adds an HTTP(S) GET source, fetched during Build and each Reload. It accepts at most one options value. WithWatch enables periodic reloads. Redirects must retain the original scheme and host (including port).

func (*Builder[T]) Build

func (b *Builder[T]) Build(ctx context.Context) (*ConfigManager[T], error)

func (*Builder[T]) MustBuild

func (b *Builder[T]) MustBuild() *ConfigManager[T]

func (*Builder[T]) WithCreateIfMissing

func (b *Builder[T]) WithCreateIfMissing() *Builder[T]

WithCreateIfMissing initializes missing AddFile sources from default tags at Build time. Existing files are never overwritten. Environment/CLI values are not written to disk. Reload does not recreate files deleted after startup.

func (*Builder[T]) WithPollInterval

func (b *Builder[T]) WithPollInterval(interval time.Duration) *Builder[T]

WithPollInterval sets the remote refresh interval used with WithWatch (default 30 seconds). HTTP sources are polled; subscribed providers are re-read to recover from missed notifications. Each tick reloads all sources serially.

func (*Builder[T]) WithTag

func (b *Builder[T]) WithTag(tag string) *Builder[T]

WithTag selects one decoding tag for all sources (default: json). Use "koanf" for existing koanf-tagged structs, including squash tags.

func (*Builder[T]) WithWatch

func (b *Builder[T]) WithWatch() *Builder[T]

WithWatch watches AddFile sources, polls AddURL sources, and subscribes to WatchProvider sources. The Build context bounds watcher lifetime.

type ConfigError

type ConfigError = typedcfg.ConfigError

type ConfigManager

type ConfigManager[T any] struct {
	// contains filtered or unexported fields
}

ConfigManager publishes validated snapshots. Get is safe concurrently with Reload and Close when callers treat Get results as read-only.

func Load

func Load[T any](paths ...string) (*ConfigManager[T], error)

Load loads JSON/YAML files in order. With no files it applies defaults and validation. Use NewBuilder for environment variables, custom providers, or file watching. Load does not start watchers or retain open files; Close is optional for a one-time startup load.

func MustBuild

func MustBuild[T any](paths ...string) *ConfigManager[T]

MustBuild is an alias for MustLoad for file-only configurations.

func MustLoad

func MustLoad[T any](paths ...string) *ConfigManager[T]

MustLoad is Load with panic-on-error semantics for application startup. Read, parse, default, and validation errors all panic. For one-time use:

cfg := typedcfg.MustLoad[Config]("config.yaml").Get()

func (*ConfigManager[T]) Close

func (cm *ConfigManager[T]) Close() error

Close stops and joins the watcher and prevents subsequent Reload calls. It also cancels in-flight AddURL requests. Concurrent/repeated calls are safe. Custom provider reads already in progress must finish before Close returns; providers own their I/O timeouts/resources.

func (*ConfigManager[T]) Errors

func (cm *ConfigManager[T]) Errors() <-chan error

Errors reports asynchronous watch/reload failures without logging globally. One pending error is buffered; additional errors may be dropped until consumed. The channel closes when watching stops or Close returns.

func (*ConfigManager[T]) Get

func (cm *ConfigManager[T]) Get() *T

Get returns the shared current snapshot without copying or allocating. Treat the entire object graph as read-only, including maps, slices and pointers. Use Snapshot when a mutable private copy is needed. Later reloads do not modify previously returned snapshots. A nil/uninitialized manager returns nil.

func (*ConfigManager[T]) Reload

func (cm *ConfigManager[T]) Reload() error

Reload re-reads all sources into a fresh configuration. Failures leave the previous snapshot intact. Reloads are serialized, including automatic reloads.

func (*ConfigManager[T]) Snapshot

func (cm *ConfigManager[T]) Snapshot() *T

Snapshot returns an independent deep copy of the current configuration. Mutating it does not affect the manager or other readers. Sharing the same copy between goroutines still requires synchronization for writes.

type ErrorType

type ErrorType = typedcfg.ErrorType

type URLOptions

type URLOptions struct {
	Timeout  time.Duration
	MaxBytes int64
	Headers  http.Header
	Client   *http.Client
	Parser   koanf.Parser
}

URLOptions configures an HTTP source. Zero values use a 10-second timeout, a 1 MiB response limit, and the default HTTP transport. Parser is inferred from the URL path's .json/.yaml/.yml extension unless explicitly supplied.

type WatchProvider

type WatchProvider interface {
	Watch(notify func(error)) (stop func() error, err error)
}

WatchProvider optionally subscribes a source to change notifications. Watch must return promptly; notify must be safe to call concurrently. A nil error requests a reload, errors are forwarded to Errors before reloading. The returned stop function releases the subscription, not the provider itself. On failure, Watch must clean up any partial subscription before returning.

Directories

Path Synopsis
adapters
cliv3
Package cliv3 adapts explicitly selected urfave/cli/v3 flags to typedcfg.
Package cliv3 adapts explicitly selected urfave/cli/v3 flags to typedcfg.
example
cli command

Jump to

Keyboard shortcuts

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