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 sources

Entry 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 API
  • github.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 .env format files
    • internal/encoding/json — codec for JSON config files (wraps stdlib encoding/json)
    • internal/encoding/toml — codec for TOML config files (wraps go-toml/v2)
    • internal/encoding/yaml — codec for YAML config files (wraps go.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 viper package depends on internal/encoding/* and internal/features — there is no mid-layer. The remote/ module is a deliberate dependency inversion: the root package defines the RemoteConfigProvider interface and remote/ registers concrete implementations, avoiding the remote dependency being pulled into the main module.

Build system#

  • Build tool: make (GNU Make), with gotestsum for test running and golangci-lint for linting
  • Key targets:
    • make test — runs all tests with -race and coverage, using gotestsum with JUnit XML output
    • make lint — runs golangci-lint (Go) and yamllint (YAML)
    • make checktest + lint
    • make fmt — auto-fixes lint issues via golangci-lint --fix
    • make deps — installs gotestsum, golangci-lint, and yamllint
  • 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#

  1. 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 own go.mod, Viper keeps the main module dependency footprint minimal while still shipping the feature officially. Consumers opt-in by adding github.com/spf13/viper/remote as an explicit dependency.

  2. 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.go with the flag set to false (using //go:build !<tag>) and a corresponding file with the flag set to true (using //go:build <tag>). This allows experimental APIs to be tested in production before stabilization without polluting the default surface.

  3. Codec registry in root package, codecs in internal/: The encoding/decoding registry lives in encoding.go (root), while implementations live in internal/encoding/*. This means codec format support is extensible via the registry without exposing the individual codec packages — a clean separation of mechanism and implementation.

  4. 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 a cmd/ tree when the project is never compiled as a standalone binary.

  5. finder.go alongside file.go for config discovery: Two parallel implementations of config file discovery coexist: the legacy file.go (uses afero + manual search paths) and the experimental finder.go (uses locafero for XDG-compliant discovery). The experimental one is gated behind the viper_finder build tag, allowing gradual migration without breaking existing users.