Documentation
¶
Overview ¶
Package configx provides the unified configuration API for Aisphere Kernel.
configx is the ONLY runtime configuration reader in Kernel. It replaces the old Kratos-derived config package and ad-hoc os.Getenv / yaml.Unmarshal patterns. Business code should depend on Config or a module-specific struct produced by Config.Scan; it should not call os.Getenv or parse YAML/JSON directly.
Design principle ¶
configx only LOADS, MERGES, and RESOLVES configuration. It does NOT validate business fields, start services, record audit, or bind to a specific config center SDK. Other Kernel modules (logx, dbx, httpx, grpcx, authx) consume configx through the stable Config / Value / Scan APIs.
configx depends only on the Go standard library plus the encodingx and logx packages; it does not import third-party config libraries.
30-second quickstart ¶
The common startup flow is:
cfg := configx.New(configx.WithSource(file.NewSource("configs/app.yaml")))
defer cfg.Close()
if err := cfg.Load(); err != nil {
return err
}
addr := configx.MustGet[string](cfg, "server.http.addr")
port := configx.GetOrDefault[int](cfg, "server.http.port", 8000)
Use Value when a single key is needed:
enabled, err := cfg.Value("features.new_home").Bool()
timeout, err := cfg.Value("upstream.timeout_ns").Duration()
Use Scan when a module owns a structured configuration section:
type HTTPConfig struct {
Addr string `json:"addr"`
Port int `json:"port"`
}
var httpCfg HTTPConfig
if err := cfg.Value("server.http").Scan(&httpCfg); err != nil {
return err
}
Sources ¶
A Source turns a backend into a slice of KeyValue values:
type Source interface {
Load() ([]*KeyValue, error)
Watch() (Watcher, error)
}
Built-in local sources live in subpackages:
file.NewSource("configs/app.yaml")
env.NewSource("KERNEL_")
Remote and platform sources live under contrib/config:
contrib/config/apollo contrib/config/nacos contrib/config/etcd contrib/config/consul contrib/config/kubernetes contrib/config/polaris
Each contrib source returns configx.Source so it can be dropped into WithSource without adapting business code.
Merge order ¶
Sources are loaded in the order passed to WithSource. Later sources override earlier leaf values while nested maps are merged recursively. Arrays are replaced as a whole. The recommended order is defaults first, environment or deployment-specific overrides last:
cfg := configx.New(configx.WithSource(
file.NewSource("configs/common.yaml"),
file.NewSource("configs/prod.yaml"),
env.NewSource("KERNEL_"),
))
Placeholder resolution ¶
The default resolver expands placeholders in string values:
${KEY}
${KEY:default}
${server.http.addr}
WithResolveActualTypes(true) converts a whole-value placeholder into bool, int64, or float64 when possible. Mixed strings remain strings:
"${PORT}" -> 8080
"http://:${PORT}" -> "http://:8080"
Placeholders are resolved after merge, so a placeholder in source A can reference a value defined in source B.
Value API ¶
Value provides typed conversions. Each method returns (T, error); none panic on type mismatch:
Bool() bool-like values (bool / "true" / "false" / numbers) Int() integer-like values (int / uint / float / numeric string) Float() floating-point values (int / uint / float / numeric string) String() string representation (any convertible type) Duration() time.Duration from integer nanoseconds Slice() []Value from []any Map() map[string]Value from map[string]any Scan(any) JSON/proto-compatible struct scan
Value is backed by an atomic store, so a cached Value reference will reflect updates after Watch or Load without needing to re-call Config.Value.
Helper API ¶
Get[T](cfg, key) returns a typed value and an error MustGet[T](cfg, key) panics on error; use only at startup GetOrDefault[T](cfg, key, v) returns fallback on missing/invalid values
Get supports bool / int / int64 / float64 / string natively. Other types fall through to Value.Scan.
Watch ¶
Watch registers one or more observers for a key. Observers are called when a loaded or watched config update changes that key. Observer callbacks should be short and non-blocking; use them to update local atomic settings or send a lightweight signal, not to perform network calls.
cfg.Watch("log.level", func(_ string, v Value) {
level, _ := v.String()
logLevel.Store(level)
})
Watch requires the key to exist at registration time; this fails fast on startup misconfiguration rather than silently never firing.
Errors ¶
ErrNotFound key is absent ErrClosed Config was closed ErrInvalidObserver Watch was called with nil observer ErrNilConfig helper received a nil Config
These errors are not business errors. Do not wrap them in errorx; surface them at startup as fail-fast failures.
Forbidden patterns ¶
Do not read os.Getenv in business code. Do not parse YAML/JSON inside every module. Do not hide config errors by silently using zero values for required fields. Do not do expensive work in Watch callbacks.
Test code is exempt: it may use os.Setenv to construct env source fixtures.
Migration from old config package ¶
Replace:
import "github.com/aisphereio/kernel/config"
cfg := config.New(config.WithSource(file.NewSource("app.yaml")))
with:
import "github.com/aisphereio/kernel/configx"
cfg := configx.New(configx.WithSource(file.NewSource("app.yaml")))
The old config/ package has been removed; new code must use configx/ and its subpackages configx/file and configx/env.
Documentation ¶
See configx/README.md for the single-source-of-truth user guide, and docs/ai/configx.md for the AI coding recipe. docs/contracts/configx.md lists behaviors that cannot change without a major version bump.
Example (BusinessActualTypedPlaceholder) ¶
cfg := New(
WithSource(exampleSource{format: "json", data: `{"PORT":"9000","server":{"port":"${PORT}"}}`}),
WithResolveActualTypes(true),
)
defer cfg.Close()
_ = cfg.Load()
fmt.Println(MustGet[int](cfg, "server.port") + 1)
Output: 9001
Example (BusinessBootstrapHTTPServer) ¶
cfg := New(WithSource(exampleSource{format: "json", data: `{"server":{"http":{"addr":":8000"}}}`}))
defer cfg.Close()
_ = cfg.Load()
addr := MustGet[string](cfg, "server.http.addr")
fmt.Println("listen", addr)
Output: listen :8000
Example (BusinessCustomDecoderForDotEnv) ¶
decoder := func(kv *KeyValue, target map[string]any) error {
for _, line := range strings.Split(string(kv.Value), "\n") {
key, value, ok := strings.Cut(line, "=")
if ok {
target[key] = value
}
}
return nil
}
cfg := New(
WithSource(exampleSource{key: ".env", data: "APP_NAME=kernel\nAPP_ENV=dev"}),
WithDecoder(decoder),
)
defer cfg.Close()
_ = cfg.Load()
fmt.Println(MustGet[string](cfg, "APP_NAME"), MustGet[string](cfg, "APP_ENV"))
Output: kernel dev
Example (BusinessFeatureFlag) ¶
cfg := New(WithSource(exampleSource{format: "json", data: `{"features":{"new_home":"true"}}`}))
defer cfg.Close()
_ = cfg.Load()
enabled, _ := cfg.Value("features.new_home").Bool()
fmt.Println("new_home", enabled)
Output: new_home true
Example (BusinessLayeredFileAndEnv) ¶
cfg := New(WithSource(
exampleSource{format: "json", data: `{"app":{"name":"kernel","env":"dev"}}`},
exampleSource{key: "app.env", data: "prod"},
))
defer cfg.Close()
_ = cfg.Load()
fmt.Println(MustGet[string](cfg, "app.name"), MustGet[string](cfg, "app.env"))
Output: kernel prod
Example (BusinessObserveRuntimeChange) ¶
cfg := New(WithSource(exampleSource{format: "json", data: `{"log":{"level":"info"}}`}))
defer cfg.Close()
_ = cfg.Load()
_ = cfg.Watch("log.level", func(_ string, value Value) {
level, _ := value.String()
fmt.Println("reload", level)
})
cfg.(*config).Value("log.level").Store("debug")
cfg.(*config).notify("log.level", cfg.Value("log.level"))
Output: reload debug
Example (BusinessResolvePlaceholder) ¶
cfg := New(WithSource(exampleSource{format: "json", data: `{"HOST":"127.0.0.1","server":{"addr":"${HOST}:8000"}}`}))
defer cfg.Close()
_ = cfg.Load()
fmt.Println(MustGet[string](cfg, "server.addr"))
Output: 127.0.0.1:8000
Example (BusinessScanDatabaseConfig) ¶
cfg := New(WithSource(exampleSource{format: "json", data: `{"database":{"driver":"postgres","max_idle":8}}`}))
defer cfg.Close()
_ = cfg.Load()
var db struct {
Driver string `json:"driver"`
MaxIdle int `json:"max_idle"`
}
_ = cfg.Value("database").Scan(&db)
fmt.Println(db.Driver, db.MaxIdle)
Output: postgres 8
Example (BusinessUpstreamTimeoutConfig) ¶
cfg := New(WithSource(exampleSource{format: "json", data: `{"upstream":{"timeout_ns":5000000000}}`}))
defer cfg.Close()
_ = cfg.Load()
timeout, _ := cfg.Value("upstream.timeout_ns").Duration()
fmt.Println(timeout.String())
Output: 5s
Example (BusinessValidateRequiredConfig) ¶
cfg := New(WithSource(exampleSource{format: "json", data: `{}`}))
defer cfg.Close()
_ = cfg.Load()
_, err := Get[string](cfg, "jwt.secret")
fmt.Println(err == ErrNotFound)
Output: true
Index ¶
- Variables
- func Get[T any](c Config, key string) (T, error)
- func GetOrDefault[T any](c Config, key string, fallback T) T
- func MustGet[T any](c Config, key string) T
- type Config
- type Decoder
- type KeyValue
- type Merge
- type Observer
- type Option
- type Reader
- type Resolver
- type Source
- type Value
- type Watcher
Examples ¶
- Package (BusinessActualTypedPlaceholder)
- Package (BusinessBootstrapHTTPServer)
- Package (BusinessCustomDecoderForDotEnv)
- Package (BusinessFeatureFlag)
- Package (BusinessLayeredFileAndEnv)
- Package (BusinessObserveRuntimeChange)
- Package (BusinessResolvePlaceholder)
- Package (BusinessScanDatabaseConfig)
- Package (BusinessUpstreamTimeoutConfig)
- Package (BusinessValidateRequiredConfig)
- Get
- GetOrDefault
- MustGet
- New
- WithDecoder
- WithMergeFunc
- WithResolveActualTypes
- WithResolver
- WithSource
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotFound is returned when a key does not exist in the loaded config tree. ErrNotFound = errors.New("configx: key not found") // ErrClosed is returned when a closed Config is used for a new load/watch operation. ErrClosed = errors.New("configx: config is closed") // ErrInvalidObserver is returned when Watch is called with a nil observer. ErrInvalidObserver = errors.New("configx: observer is nil") // ErrNilConfig is returned by helper functions when the Config argument is nil. ErrNilConfig = errors.New("configx: config is nil") )
Functions ¶
func Get ¶
Get retrieves a config value by key and scans it into the target type.
Example ¶
cfg := New(WithSource(exampleSource{
format: "json",
data: `{"server":{"port":8080}}`,
}))
defer cfg.Close()
_ = cfg.Load()
port, _ := Get[int](cfg, "server.port")
fmt.Println(port)
Output: 8080
func GetOrDefault ¶
GetOrDefault returns a typed value if present; otherwise it returns fallback.
Example ¶
cfg := New(WithSource(exampleSource{
format: "json",
data: `{"server":{"addr":":8000"}}`,
}))
defer cfg.Close()
_ = cfg.Load()
fmt.Println(GetOrDefault[int](cfg, "server.port", 8000))
Output: 8000
Types ¶
type Config ¶
type Config interface {
Load() error
Scan(v any) error
Value(key string) Value
Watch(key string, o Observer) error
Close() error
}
Config is the runtime configuration interface used by kernel modules and apps.
type Option ¶
type Option func(*options)
Option is config option.
func WithDecoder ¶
WithDecoder with config decoder. DefaultDecoder behavior: If KeyValue.Format is non-empty, then KeyValue.Value will be deserialized into map[string]interface{} and stored in the config cache(map[string]interface{}) if KeyValue.Format is empty,{KeyValue.Key : KeyValue.Value} will be stored in config cache(map[string]interface{})
Example ¶
decoder := func(kv *KeyValue, target map[string]any) error {
parts := strings.SplitN(string(kv.Value), "=", 2)
if len(parts) == 2 {
target[parts[0]] = parts[1]
}
return nil
}
cfg := New(
WithSource(exampleSource{key: "app.name", data: "name=kernel"}),
WithDecoder(decoder),
)
defer cfg.Close()
_ = cfg.Load()
name, _ := Get[string](cfg, "name")
fmt.Println(name)
Output: kernel
func WithMergeFunc ¶
WithMergeFunc with config merge func.
Example ¶
replaceMerge := func(dst, src any) error {
d := dst.(*map[string]any)
*d = src.(map[string]any)
return nil
}
cfg := New(
WithSource(
exampleSource{format: "json", data: `{"name":"first"}`},
exampleSource{format: "json", data: `{"name":"second"}`},
),
WithMergeFunc(replaceMerge),
)
defer cfg.Close()
_ = cfg.Load()
fmt.Println(MustGet[string](cfg, "name"))
Output: second
func WithResolveActualTypes ¶
WithResolveActualTypes with config resolver. bool input will enable conversion of config to data types
Example ¶
cfg := New(
WithSource(exampleSource{
format: "json",
data: `{"PORT":"8080","server":{"port":"${PORT}"}}`,
}),
WithResolveActualTypes(true),
)
defer cfg.Close()
_ = cfg.Load()
port, _ := Get[int](cfg, "server.port")
fmt.Printf("%T %d\n", port, port)
Output: int 8080
func WithResolver ¶
WithResolver with config resolver.
Example ¶
resolver := func(values map[string]any) error {
values["service"] = map[string]any{"name": "resolved"}
return nil
}
cfg := New(
WithSource(exampleSource{format: "json", data: `{}`}),
WithResolver(resolver),
)
defer cfg.Close()
_ = cfg.Load()
fmt.Println(MustGet[string](cfg, "service.name"))
Output: resolved
func WithSource ¶
WithSource with config source.
Example ¶
cfg := New(WithSource(
exampleSource{format: "json", data: `{"app":{"name":"kernel"}}`},
exampleSource{format: "json", data: `{"app":{"env":"dev"}}`},
))
defer cfg.Close()
_ = cfg.Load()
name, _ := Get[string](cfg, "app.name")
env, _ := Get[string](cfg, "app.env")
fmt.Println(name, env)
Output: kernel dev
type Reader ¶
type Reader interface {
Merge(...*KeyValue) error
Value(string) (Value, bool)
Source() ([]byte, error)
Resolve() error
}
Reader is the internal merged config tree reader.
type Value ¶
type Value interface {
Bool() (bool, error)
Int() (int64, error)
Float() (float64, error)
String() (string, error)
Duration() (time.Duration, error)
Slice() ([]Value, error)
Map() (map[string]Value, error)
Scan(any) error
Load() any
Store(any)
}
Value is a typed view over one config value.