Documentation
¶
Overview ¶
Package config provides a strict, type-safe configuration loader that sources values from hardcoded defaults and environment variables.
Design philosophy ¶
This package is an opinionated infra-layer tool, not a general-purpose configuration library. Every decision prioritises catching misconfiguration at startup rather than silently accepting a degraded runtime state.
Key properties:
- Two sources only: defaults (map[string]any) + env vars. No file sources. In containerised / twelve-factor deployments env vars are the standard runtime knob; files add operational complexity (mounts, permissions, drift).
- Strict typing: WeaklyTypedInput is false. Accepting "1" where int is expected hides bugs; explicit conversion must happen at the source.
- Unknown keys are rejected before decoding. A typo in an env var name is caught immediately with a clear error instead of being silently ignored.
- Every exported struct field must carry an explicit koanf tag. Relying on lowercase fallback names is fragile and hides intent.
- Cross-field validation is supported via an optional Validate() error method on *T (pointer receiver).
Environment variable naming ¶
Double-underscore (__) is used as a hierarchy separator because a single underscore is too common inside segment names (e.g. DB_MAX_CONNS would be ambiguous). Examples with prefix "APP_":
APP__DEBUG=true -> debug APP__DB__HOST=localhost -> db.host APP__ENTRY__HTTP__PORT=8080 -> entry.http.port
The prefix is normalised internally: "APP" and "APP_" are equivalent.
Struct tag contract ¶
All exported fields must define a koanf tag explicitly:
Name string `koanf:"name"` // scalar leaf Sub Inner `koanf:"sub"` // nested struct Base `koanf:",squash"` // flatten into parent namespace ignored string `koanf:"-"` // excluded from config loading
Only the "squash" option is supported. Other mapstructure options (omitempty, remain, etc.) are not meaningful in an env-only strict config layer and are intentionally not supported.
Validation ¶
Validation runs in three phases:
- Struct tag validation via go-playground/validator (e.g. `validate:"required,min=1"`).
- Cross-field semantic validation via Validate() error on *T, if implemented. Use this for constraints that cannot be expressed with struct tags, such as "TLSCert must be set when TLS is enabled". Validate() must be implemented on a pointer receiver.
- errors.Join is recommended inside Validate() to surface all errors at once rather than stopping at the first failure.
Index ¶
Constants ¶
const ( AppRunmodeDev = "dev" AppRunmodeProd = "prod" )
Runmode constants define the supported application run modes. They are intentionally kept as untyped string constants so that they can be compared directly against the string value decoded from config without a type conversion.
const ( LogFormatJSON = "json" LogFormatConsole = "console" // LogFormatAuto resolves to [LogFormatConsole] in dev runmode and to // [LogFormatJSON] in prod runmode. Resolution happens at runtime via // [Base.GetLogFormat]; the raw "auto" value is never passed to the logger. LogFormatAuto = "auto" )
Log format constants define the supported log output formats.
const ( LogMinLevelDebug = "debug" LogMinLevelInfo = "info" LogMinLevelWarn = "warn" LogMinLevelError = "error" LogMinLevelPanic = "panic" LogMinLevelFatal = "fatal" )
Log level constants define the supported minimum log severity levels in ascending order of severity.
const MinUnprivilegedPort = 1024
MinUnprivilegedPort is the lowest TCP/UDP port number that can be bound without root (CAP_NET_BIND_SERVICE) privileges on Linux. Used as the lower bound in HTTP.Port validation.
Variables ¶
This section is empty.
Functions ¶
func New ¶
New loads, validates, and returns a fully-populated config struct of type T.
Loading order (last writer wins):
- Hardcoded defaults supplied by the caller.
- Environment variables with the given prefix.
Both sources are subject to the same unknown-key guard: a key present in defaults that does not map to a field in T is rejected just like a bad env var.
T must be a struct. All exported fields must carry an explicit koanf tag; see package documentation for the full tag contract.
envPrefix is normalised internally: trailing underscores are stripped and exactly one is appended, so "APP" and "APP_" are equivalent.
Types ¶
type App ¶
type App struct {
// Name is the human-readable service identifier used in logs and traces.
Name string `koanf:"name" validate:"required"`
// Runmode controls environment-specific behaviour such as log format
// resolution and debug-mode gating. Must be one of [AppRunmodeDev] or [AppRunmodeProd].
Runmode string `koanf:"runmode" validate:"required,oneof=dev prod"`
}
App holds identity and runtime-mode configuration for the service.
type Base ¶
type Base struct {
App App `koanf:"app" validate:"required"`
Log Log `koanf:"log" validate:"required"`
Entry Entry `koanf:"entry" validate:"required"`
// Debug enables verbose developer tooling such as stack traces and
// human-readable log output. When true:
// - runmode must be "dev" (debug in prod is forbidden by [Base.Validate])
// - log.min_level must be "debug"
Debug bool `koanf:"debug"`
}
Base holds the configuration fields shared by every service. It is intended to be embedded with koanf:",squash" so that its fields appear at the root of the config namespace rather than under a "base." prefix.
func (Base) GetLogFormat ¶
GetLogFormat resolves the effective log format, expanding LogFormatAuto to a concrete value based on the current runmode. All other format values are returned as-is. Use this method when constructing the logger - never read Log.Format directly.
func (Base) Validate ¶
Validate enforces cross-field constraints that cannot be expressed with struct tags alone. It is called automatically by New as part of semantic validation phase.
Rules:
- Debug mode is forbidden in prod runmode: it exposes internals and degrades performance.
- When debug is enabled, log.min_level must be "debug": any higher level would silently swallow the debug output that debug mode is meant to show.
type Entry ¶
type Entry struct {
HTTP HTTP `koanf:"http" validate:"required"`
}
Entry holds inbound traffic entrypoint configuration.
type HTTP ¶
type HTTP struct {
// Port is the TCP port the server listens on.
// Must be in the unprivileged range [1024, 65535].
Port uint16 `koanf:"port" validate:"required,min=1024,max=65535"`
// WriteTimeout is the maximum time to write a complete response.
// Must be greater than RequestTimeout.
WriteTimeout time.Duration `koanf:"write_timeout" validate:"required,gte=5s,lte=90s,gtfield=RequestTimeout"`
// ReadTimeout is the maximum time to read a complete request including body.
ReadTimeout time.Duration `koanf:"read_timeout" validate:"required,gte=5s,lte=60s"`
// IdleTimeout is the maximum time to keep an idle keep-alive connection open.
// Must be greater than RequestTimeout.
IdleTimeout time.Duration `koanf:"idle_timeout" validate:"required,gte=30s,lte=180s,gtfield=RequestTimeout"`
// RequestTimeout is the context deadline injected into each request handler.
// Must be greater than ReadTimeout so the handler has a meaningful execution
// budget after the full request has been read.
RequestTimeout time.Duration `koanf:"request_timeout" validate:"required,gte=10s,lte=120s,gtfield=ReadTimeout"`
}
HTTP holds configuration for the HTTP server entrypoint.
Timeout ordering is strictly enforced:
ReadTimeout < RequestTimeout < WriteTimeout < IdleTimeout
Rationale for each constraint:
ReadTimeout 5s–60s - time to read the full request including body.
A low ceiling prevents slow-loris attacks.
RequestTimeout 10s–120s - context deadline injected into each handler.
Must exceed ReadTimeout so the handler has a
meaningful budget after the request is fully read.
WriteTimeout 5s–90s - time to write the full response.
Must exceed RequestTimeout so the server can
flush the response after the handler completes.
IdleTimeout 30s–180s - time to keep an idle keep-alive connection open.
Must exceed RequestTimeout so that a connection
is not closed while a request is still in flight.
type Log ¶
type Log struct {
// MinLevel is the minimum severity level at which log entries are emitted.
// Entries below this level are discarded. Must be one of the LogMinLevel* constants.
MinLevel string `koanf:"min_level" validate:"required,oneof=debug info warn error panic fatal"`
// Format controls the log output encoding.
// Use [LogFormatAuto] to let runmode decide; use [Base.GetLogFormat] to read
// the resolved value.
Format string `koanf:"format" validate:"required,oneof=json auto console"`
}
Log holds logging configuration.