go-envy is a lean, production‑ready configuration library that gives you:
• Multiple file loads (JSON/YAML) with deep‑merge (maps) & replace (arrays)
• Case‑insensitive dot‑paths + array indexes:db.host,addresses[1].area
• Predictable precedence: RuntimeSetConfig→ Environment → Files
• Typed getters (GetString,GetInt, …), thread‑safe reads/writes
• Minimal deps: stdlib + YAML org’s maintainedgo.yaml.in/yaml/v4Built for clarity, safety, and performance in real services.
go get github.com/vyntro/go-envyGo: 1.25.6
go-envy/
├─ go.mod
├─ LICENSE
├─ README.md
├─ envy.go # public API, constructor & options, typed getters
├─ loader.go # JSON/YAML decoding & normalization (map[string]any, arrays allowed)
├─ merge.go # deep-merge (maps recursive), arrays replaced wholesale
├─ path.go # dot-path (+ [index]) parsing; case-insensitive map keys
├─ env.go # env mapping & coercion, deep-copy helpers
├─ types.go # Options & Option types
└─ envy_test.go # unit tests including array paths and precedence
config.yaml
db:
host: localhost
port: 5432
ssl: false
addresses:
- street: 1
area: michi
- street: North street
area: Kichi
- street: Sout street
area: Sichimain.go
package main
import (
"fmt"
"github.com/vyntro/go-envy"
)
func main() {
cfg := envy.New(
envy.WithEnvEnabled(true), // enable ENV overlay (default: true)
envy.WithEnvPrefix("ENVY_"), // default ENV prefix
envy.WithEnvCoerceTypes(true), // coerce ENV values to target type
envy.WithMissingBehavior(""), // on missing GetConfig without default, return ""
)
must(cfg.Load("config.yaml")) // load as many files as you need
// Runtime override (highest precedence)
cfg.SetConfig("db.port", 6432)
// Dot paths + array indexes
host := cfg.GetString("db.host", "localhost")
first := cfg.GetConfig("addresses[0]").(map[string]any)
area1 := cfg.GetString("addresses[1].area") // "Kichi"
fmt.Println(host, first["street"], area1)
// Effective config (files -> env -> runtime)
eff := cfg.GetAllConfig()
fmt.Println(eff)
}
func must(err error) { if err != nil { panic(err) } }- Prefix:
ENVY_(configurable viaWithEnvPrefix). - Mapping: replace
.and-with_and uppercase the path. - Array indexes are digits:
addresses[1].area→ENVY_ADDRESSES_1_AREA. - Unset ENV means no override. ENV keys not in files are allowed and will be created on the fly.
Caching note:
- The library builds an internal, read-optimized snapshot of environment variables to avoid re-scanning the process environment on every lookup. By default the snapshot contains only variables that match the configured
EnvPrefix(case-insensitive). This makesGetAllConfigand other lookups much faster in read-heavy workloads. - If your process modifies environment variables at runtime (for example via
os.Setenv), you have two options:-
Call
RefreshEnvCache()on the*Configinstance to force a full rescan and rebuild the snapshot (recommended after programmatic sets). Example:cfg.RefreshEnvCache()
-
Rely on the library's per-key fallback: when a requested env key is not present in the snapshot, the library will perform a direct
os.LookupEnvand update the snapshot for that key lazily. This is convenient but slightly more expensive for the first lookup.
-
The snapshot is internal and read-only; the library never calls os.Setenv. Tests and examples may call os.Setenv to simulate overrides, but production code should call RefreshEnvCache() if it programmatically mutates environment variables and needs immediate visibility.
- The library takes a lightweight snapshot of the process environment on first use to avoid repeatedly scanning
os.Environ()on hot code paths. This makes environment lookups fast for read-heavy workloads. - If your process modifies environment variables at runtime (for example via
os.Setenv), there are two ways to ensure the library sees those changes:- Call
RefreshEnvCache()on your*Configinstance to force a full rescan ofos.Environ()and update the internal snapshot. - The library also performs a per-key fallback check (via
os.LookupEnv) when an env key is missing from the snapshot, and will update the cache for that key automatically. This keeps behavior intuitive while still avoiding a full rescan on every call.
- Call
- Recommendation: for deterministic behavior after bulk env updates (or when running programmatic tests that change many envs), call
cfg.RefreshEnvCache()once after making the changes. For occasional single-key updates, the per-key fallback will pick them up. - Tradeoff: snapshotting avoids repeated syscalls and is faster at scale;
RefreshEnvCache()is cheap but does perform a full environment scan.
- Runtime
SetConfig(in-memory) - Environment variables
- Files (reverse load order; later loaded file overrides earlier)
Creates a new configuration instance with sane defaults and optional behavior tweaks via Options.
Defaults:
EnvEnabled: true— ENV overlay is activeEnvPrefix: "ENVY_"— ENV variables must start with this prefixEnvCoerceTypes: true— try to coerce ENV values to target type (bool/int/float)MissingBehavior: ""—GetConfigreturns empty string when key is missing and no default is passedEnvKeyReplacer: replaces.and-with_and uppercases the rest (and also treats[n]as.nbefore replacement)
What it does: Turns environment variable overrides on or off.
- When true (default), any matching
ENVY_*variables are applied as an overlay above files and below runtime. - When false, ENV variables are completely ignored by both
GetConfigandGetAllConfig.
When to use:
- Turn off for fully deterministic test environments or tooling that must ignore ENV.
- Keep on in services so ops can change behavior without code or file edits.
What it does: Sets the ENV prefix (default ENVY_). Only variables beginning with this prefix are considered.
Example: with WithEnvPrefix("APP_"), db.host maps to APP_DB_HOST.
When to use:
- Multi-service processes needing separate namespaces per component (
ORDER_,PAYMENT_).
What it does: Sets a custom path→ENV mapping function.
Default logic:
- Convert
[n]→.n(so indexes become segments) - Replace
.and-with_ - Uppercase
When to use:
- You need a different ENV naming convention (e.g., keep case, other delimiters).
What it does: Controls whether ENV values are type‑coerced to the underlying target type.
- If true (default), and the file/runtime value is, say,
int, ENV values are parsed tointwhen possible. - If false, ENV values are always strings.
When to use:
- Disable if you want strict string semantics from ENV.
What it does: Sets the return of GetConfig(field) when the key is missing and no default is provided.
""(default) → return empty string"field"→ return the field name that was requested"<literal>"→ return the literal value you provide
Notes:
- Typed getters ignore MissingBehavior; they return their default/zero.
Loads a JSON or YAML file and deep‑merges it into the base configuration.
- Maps are merged recursively — later file overrides conflicting keys.
- Arrays are replaced entirely by the later file (no per‑element merge).
- Returns descriptive errors for decode issues, trailing data, or invalid root types.
Returns a deep copy of the effective configuration after applying layers: files → ENV → runtime.
Useful for diagnostics, /config debug endpoints, or exporting the current view.
Gets a value using a case‑insensitive dot‑path with array indexes.
- Checks runtime → ENV → files.
- If found, returns the native type (
bool,int,float64,string,map[string]any,[]any, etc.). - If not found and a default string is provided, returns that default.
- If not found and no default is provided, returns MissingBehavior (default:
""), or the field name if set to"field".
Examples
cfg.GetConfig("db.host") // e.g., "localhost"
cfg.GetConfig("addresses") // []any of maps
cfg.GetConfig("addresses[0]") // map[string]any
cfg.GetConfig("addresses[1].area") // e.g., "Kichi"Sets a runtime (in‑memory) value by path. This is the highest precedence layer.
- Creates missing containers as needed.
- For arrays, setting
arr[5]expands the array to length 6 withnilfillers.
Deletes a runtime value at path.
- Map keys are removed.
- Array elements are set to
nil(array length is preserved).
These helpers return a specific type, consulting runtime → ENV → files. They ignore MissingBehavior and return your default (if provided) or the zero value.
func (c *Config) GetString(field string, defaultValue ...string) string
func (c *Config) GetInt(field string, defaultValue ...int) int
func (c *Config) GetBool(field string, defaultValue ...bool) bool
func (c *Config) GetFloat(field string, defaultValue ...float64) float64
// Duration in nanoseconds; accepts numeric ns or time.ParseDuration strings
func (c *Config) GetDuration(field string, defaultValue ...int64) int64Examples
cfg.GetString("db.host", "localhost")
cfg.GetInt("db.port", 5432)
cfg.GetBool("db.ssl", false)
cfg.GetFloat("service.timeoutSec", 5.0)
cfg.GetDuration("cache.ttl", int64(time.Second))- Dot segments → map/object keys:
server.tls.enabled - Brackets → array index:
addresses[2].area - Numeric dot segment → also array index:
addresses.2.area - Case‑insensitive for map keys (not for indices):
DB.Host==db.host. - Invalid paths return missing behavior (or defaults in typed getters).
- Maps: deep‑merge recursively; later file overrides only the keys it provides.
- Arrays: later file replaces the prior array at that key.
- Runtime
SetConfig— high‑priority, in‑memory, not persisted - Environment variables —
ENVY_prefix (configurable), path →UPPER_SNAKE, indices as digits - Files — order of
Load; later files override earlier
- Unsupported extension: Only
.json,.yaml,.ymlare accepted. - Invalid root: Root must be an object (map).
- Array operations: Setting an out‑of‑range index expands with
nil. Deleting an element setsnil.
MIT
This project is open source. Contributions of all sizes are welcome—bug fixes, tests, docs, examples, and new features. Please open an issue to discuss ideas or submit a pull request when you are ready.