Skip to content

go-scope-config

Go Reference Go Report Card Documentation

Table of Contents

Go package for loading environment-based configurations (SCOPE) using Viper.

This package is particularly well-suited for Kubernetes environments, where the SCOPE variable (e.g., dev, staging, prod) can be easily injected into Pods as an environment variable, allowing the application to automatically pick the correct configuration file based on the cluster environment.

The environment variable name (SCOPE) is configurable to support different organizational standards.

Installation

go get github.com/arielsrv/go-scope-config

Usage

The package automatically looks for the SCOPE environment variable. If it's not defined, it uses dev by default.

It searches for files with the pattern config.[SCOPE].yaml or config.[SCOPE].yml in a folder (default is config/).

Configuration Merging (Inheritance)

If a config.common.yaml (or .yml) exists in the configuration directory, it will be loaded first as a base configuration. Then, the scope-specific file (config.[SCOPE].yaml) will be merged into it, overriding any shared keys.

graph TD
    Start([Start Load]) --> InitViper[Initialize Viper]
    InitViper --> LoadEnv[AutomaticEnv enabled]
    LoadEnv --> CheckCommon{config.common.yaml?}
    CheckCommon -- Exists --> ReadCommon[Read common config]
    CheckCommon -- Not Found --> CheckScope
    ReadCommon --> CheckScope{config.SCOPE.yaml?}
    CheckScope -- Exists --> MergeScope[Merge scope config]
    CheckScope -- Not Found --> Error([Error: Scope config not found])
    MergeScope --> FinalConfig([Final Configuration])

    subgraph Merging Logic
        ReadCommon
        MergeScope
    end

    style ReadCommon fill:#f9f,stroke:#333,stroke-width:2px
    style MergeScope fill:#bbf,stroke:#333,stroke-width:2px
    style Error fill:#fbb,stroke:#333,stroke-width:2px

Example File Structure

.
├── config/
│   ├── config.common.yaml (base values)
│   ├── config.dev.yaml    (overrides for dev)
│   └── config.prod.yml    (overrides for prod)
└── main.go

Code

Using LoadDefault (Autoloader)

The quickest way to start with default options:

package main

import (
    "fmt"
    "log/slog"
    "os"
    goscopeconfig "github.com/arielsrv/go-scope-config"
)

func main() {
    // Local logger to avoid using the global slog
    logger := slog.Default()

    // Loads from "config/" folder using SCOPE env var (defaults to "dev")
    v, err := goscopeconfig.LoadDefault()
    if err != nil {
        logger.Error("Error loading default config", "error", err)
        os.Exit(1)
    }

    fmt.Printf("App Name: %s\n", v.GetString("app.name"))
}

Automatic Autoloader (init function)

The package provides two ways to automatically load the configuration.

1. Using DefaultViper (named import)

If you import the package with a name, you can access the pre-initialized DefaultViper instance.

package main

import (
    "fmt"
    "log/slog"
    "os"
    goscopeconfig "github.com/arielsrv/go-scope-config"
)

func main() {
    // Local logger to avoid using the global slog
    logger := slog.Default()

    // Check if there was an error during automatic loading
    if goscopeconfig.ErrLoad != nil {
        logger.Error("Error loading config", "error", goscopeconfig.ErrLoad)
        os.Exit(1)
    }

    // Use the pre-loaded instance
    v := goscopeconfig.DefaultViper
    fmt.Printf("App Name: %s\n", v.GetString("app.name"))
}

2. Using Blank Import (global Viper)

If you want to use the standard github.com/spf13/viper package directly, you can use the autoload sub-package with a blank import. This will load the configuration into Viper's global instance (the singleton returned by viper.GetViper()).

[!IMPORTANT] This is distinct from the DefaultViper provided by the root package, which uses its own isolated Viper instance.

package main

import (
    "fmt"
    _ "github.com/arielsrv/go-scope-config/autoload"
    "github.com/spf13/viper"
)

func main() {
    // Configuration is already loaded into global viper
    fmt.Printf("App Name: %s\n", viper.GetString("app.name"))
}

Manual Initialization (With Options)

package main

import (
    "fmt"
    "log/slog"
    "os"
    goscopeconfig "github.com/arielsrv/go-scope-config"
)

func main() {
    // Local logger to avoid using the global slog
    logger := slog.Default()

    // Initializes the loader. 
    // By default, it looks in the "config/" folder and reads the
    // "SCOPE" environment variable.
    loader := goscopeconfig.New(
        goscopeconfig.WithConfigDir("custom_configs"),
        goscopeconfig.WithScopeEnv("APP_ENV"),
    )

    if err := loader.Load(); err != nil {
        logger.Error("Error loading config", "error", err)
        os.Exit(1)
    }

    // Access values via Viper
    v := loader.Viper()
    fmt.Printf("App Name: %s\n", v.GetString("app.name"))
    fmt.Printf("Current Scope: %s\n", loader.GetScope())
}

Architecture

The ConfigLoader is the core component. It uses options for configuration:

Loader API

  • loader.Load(): Executes the loading and merging.
  • loader.Viper(): Returns the internal *viper.Viper instance.
  • loader.GetScope(): Returns the current detected scope.
  • loader.GetConfigPath(): Returns the absolute path of the loaded configuration file.

Examples

You can find complete examples in the examples/ folder:

  • blank-import: Usage with blank import (global Viper).
  • autoloader: Quick start using LoadDefault().
  • automatic: Accessing the pre-initialized DefaultViper.
  • with-logger: Integrating with slog (JSON format).
  • custom-scope-env: Using a custom environment variable for scope.
  • custom-dir: Loading configurations from a non-standard directory.
  • merge-common: Demonstrating configuration inheritance and overrides.
  • uber-fx: Integration with the Uber-FX dependency injection framework.
  • docker-compose: Running inside a container with Docker Compose, demonstrating environment variable inheritance.

Run examples

The examples are in a separate module. To run them, go to the examples directory:

cd examples

Then run the desired example:

go run simple/main.go
go run custom-dir/main.go
go run merge-common/main.go
go run autoloader/main.go
go run automatic/main.go
go run blank-import/main.go
go run with-logger/main.go
go run custom-scope-env/main.go
go run uber-fx/main.go

Docker Compose Example

To run the Docker Compose example:

cd examples/docker-compose
docker compose up

Logger Support

You can provide a logger that satisfies the Logger interface (which has a Printf(format string, v ...any) method). This allows easy integration with both the standard log package and modern loggers like slog.

// Example with slog (JSON format)
handler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(handler)

// Small wrapper to satisfy the Printf interface
type slogWrapper struct { logger *slog.Logger }
func (s *slogWrapper) Printf(f string, v ...any) {
    s.logger.Info(fmt.Sprintf(f, v...))
}

loader := goscopeconfig.New(
    goscopeconfig.WithLogger(&slogWrapper{logger: logger}),
)
loader.Load()

Commands

The project uses Taskfile to manage common tasks:

  • task test: Run unit tests.
  • task lint: Run linters (golangci-lint, gofumpt, betteralign).
  • task audit: Verify modules, run go vet and govulncheck.
  • task markdown: Lint markdown files.
  • task coverage: Generate and show coverage report.
  • task clean: Remove coverage and temporary files.
  • task build: Build the project.

Environment Variables

In addition to SCOPE, the package enables Viper's AutomaticEnv(), so you can override any value from the YAML files using environment variables with the corresponding prefix (by default no prefix, with the . separator replaced by _).

Example: APP_NAME=my-app will override the value of app.name in the YAML.