Viper — Structure#
Layout pattern#
Custom / Flat with selective internal packaging
Viper is a single-package library at its root with no cmd/ binary, no pkg/ directory, and no multi-binary structure. All public API lives in the root package (github.com/spf13/viper). Supporting implementation details are segregated into internal/ sub-packages. This is a common pattern for Go libraries that expose a flat, ergonomic API while keeping codec and feature-flag internals encapsulated. The remote/ directory is a separate Go module (has its own go.mod), allowing it to carry heavier dependencies without polluting the main module.
Directory map#
viper/
├── *.go # Root package: all public API + core implementation
│ ├── viper.go # Core Viper struct, Get/Set/Bind* methods, global singleton
│ ├── encoding.go # Encoder/decoder registry, codec registration
│ ├── file.go # Config file search and reading logic
│ ├── finder.go # Experimental: locafero-based config file finder
│ ├── flags.go # pflag binding (FlagValueSet, FlagValue interfaces)
│ ├── remote.go # Remote config source hook (interface registration)
│ ├── util.go # Key normalisation, path splitting helpers
│ ├── logger.go # Logging abstraction (slog-based)
│ ├── experimental.go # ExperimentalBindStruct implementation
│ └── errors.go # Sentinel error types
├── internal/
│ ├── encoding/ # Internal codec packages (not exported)
│ │ ├── dotenv/ # dotenv format codec
│ │ ├── json/ # JSON format codec
│ │ ├── toml/ # TOML format codec
│ │ └── yaml/ # YAML format codec
│ ├── features/ # Build-tag feature flags
│ │ ├── bind_struct_default.go # BindStruct = false (default, !viper_bind_struct)
│ │ ├── bind_struct.go # BindStruct = true (build tag: viper_bind_struct)
│ │ ├── finder_default.go # Finder = false (default, !viper_finder)
│ │ └── finder.go # Finder = true (build tag: viper_finder)
│ └── testutil/ # Shared test helpers (filepath utilities)
└── remote/ # Separate Go module: remote config source adapters
├── go.mod # Separate module: github.com/spf13/viper/remote
├── remote.go # RemoteConfigProvider registration
└── filepath.go # Path helpers for remote sourcesEntry points#
Viper is a library — there are no main.go entry points. The root package is the sole entry point for consumers:
github.com/spf13/viper— primary library package with both global-singleton API and instance-based APIgithub.com/spf13/viper/remote— optional, separate module for remote config sources (etcd, Consul); imported separately by consumers who need it
Package organization#
Internal packages (
internal/):internal/encoding/dotenv— codec for.envformat filesinternal/encoding/json— codec for JSON config files (wraps stdlibencoding/json)internal/encoding/toml— codec for TOML config files (wrapsgo-toml/v2)internal/encoding/yaml— codec for YAML config files (wrapsgo.yaml.in/yaml/v3)internal/features— compile-time feature flags; each flag has a_default.go(off) and an opt-in file activated by a build tag (e.g.,viper_finder,viper_bind_struct)internal/testutil— test utilities shared across test files (filepath resolution helpers)
Public packages (
pkg/): None. Viper uses the root package as its sole public surface.Layering: Flat. The root
viperpackage depends oninternal/encoding/*andinternal/features— there is no mid-layer. Theremote/module is a deliberate dependency inversion: the root package defines theRemoteConfigProviderinterface andremote/registers concrete implementations, avoiding the remote dependency being pulled into the main module.
Build system#
- Build tool:
make(GNU Make), withgotestsumfor test running andgolangci-lintfor linting - Key targets:
make test— runs all tests with-raceand coverage, usinggotestsumwith JUnit XML outputmake lint— runsgolangci-lint(Go) andyamllint(YAML)make check—test+lintmake fmt— auto-fixes lint issues viagolangci-lint --fixmake deps— installsgotestsum,golangci-lint, andyamllint
- Docker: No Docker or docker-compose files present. Purely a library; no containerized runtime.
- CI: GitHub Actions workflows (
ci.yaml,checks.yaml,stale.yaml,octoslash.yaml); Nix flake (flake.nix/flake.lock) for reproducible dev shell.
Notable structural decisions#
remote/as a separate Go module: The remote config source package (etcd,Consul) carries dependencies that most Viper users don’t need. By placing it in its owngo.mod, Viper keeps the main module dependency footprint minimal while still shipping the feature officially. Consumers opt-in by addinggithub.com/spf13/viper/remoteas an explicit dependency.Build-tag feature flags via
internal/features: Instead of runtime feature flags or configuration, Viper uses compile-time build tags (viper_finder,viper_bind_struct) implemented through file pairs: a_default.gowith the flag set tofalse(using//go:build !<tag>) and a corresponding file with the flag set totrue(using//go:build <tag>). This allows experimental APIs to be tested in production before stabilization without polluting the default surface.Codec registry in root package, codecs in
internal/: The encoding/decoding registry lives inencoding.go(root), while implementations live ininternal/encoding/*. This means codec format support is extensible via the registry without exposing the individual codec packages — a clean separation of mechanism and implementation.No
cmd/or binary output: Viper is purely a library with zero binaries. The flat root-package layout is appropriate — there is no need for acmd/tree when the project is never compiled as a standalone binary.finder.goalongsidefile.gofor config discovery: Two parallel implementations of config file discovery coexist: the legacyfile.go(usesafero+ manual search paths) and the experimentalfinder.go(useslocaferofor XDG-compliant discovery). The experimental one is gated behind theviper_finderbuild tag, allowing gradual migration without breaking existing users.