DEV Community

Kittipat.po
Kittipat.po

Posted on • Edited on

A Guide to Configuration Management in Go with Viper

Introduction

Managing configurations efficiently is a cornerstone of building scalable and maintainable software. In Go, the Viper package 🐍 stands out as a robust solution for managing application configurations. With support for multiple file formats, environment variables, and seamless unmarshaling to structs, Viper simplifies configuration management for modern applications.

In this blog, we’ll walk through how to use Viper to load and manage configurations from different sources, map them to Go structs, and integrate environment variables dynamically.

👩‍💻 Setting Up Viper:

Let’s dive into the practical implementation of Viper in a Go application. For this guide, we’ll use a simple application configuration example with a YAML file and environment variables.

Step 1: Install the Viper Package

Start by installing Viper in your project:

go get github.com/spf13/viper
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a Configuration File

Create a config.yaml file in your project’s root directory. This file will define the default configuration for your application:

app:
  name: "MyApp"
  port: 8080
namespace: "myapp"
owner: "John Doe"
Enter fullscreen mode Exit fullscreen mode

🔨 Implementing Viper in Go

Here’s how you can use Viper in your application. Below is the example code from main.go:

package main

import (
    "fmt"
    "log"
    "strings"

    "github.com/spf13/viper"
)

type AppConfig struct {
    App struct {
        Name string `mapstructure:"name"`
        Port int    `mapstructure:"port"`
    } `mapstructure:"app"`
    NS    string `mapstructure:"namespace"`
    Owner string `mapstructure:"owner"`
}

func main() {
    // Set up viper to read the config.yaml file
    viper.SetConfigName("config") // Config file name without extension
    viper.SetConfigType("yaml")   // Config file type
    viper.AddConfigPath(".")      // Look for the config file in the current directory


    /*
        AutomaticEnv will check for an environment variable any time a viper.Get request is made.
        It will apply the following rules.
            It will check for an environment variable with a name matching the key uppercased and prefixed with the EnvPrefix if set.
    */
    viper.AutomaticEnv()
    viper.SetEnvPrefix("env")                              // will be uppercased automatically
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // this is useful e.g. want to use . in Get() calls, but environmental variables to use _ delimiters (e.g. app.port -> APP_PORT)

    // Read the config file
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("Error reading config file, %s", err)
    }

    // Set up environment variable mappings if necessary
    /*
        BindEnv takes one or more parameters. The first parameter is the key name, the rest are the name of the environment variables to bind to this key.
        If more than one are provided, they will take precedence in the specified order. The name of the environment variable is case sensitive.
        If the ENV variable name is not provided, then Viper will automatically assume that the ENV variable matches the following format: prefix + "_" + the key name in ALL CAPS.
        When you explicitly provide the ENV variable name (the second parameter), it does not automatically add the prefix.
            For example if the second parameter is "id", Viper will look for the ENV variable "ID".
    */
    viper.BindEnv("app.name", "APP_NAME") // Bind the app.name key to the APP_NAME environment variable

    // Get the values, using env variables if present
    appName := viper.GetString("app.name")
    namespace := viper.GetString("namespace") // AutomaticEnv will look for an environment variable called `ENV_NAMESPACE` ( prefix + "_" + key in ALL CAPS)
    appPort := viper.GetInt("app.port")       // AutomaticEnv will look for an environment variable called `ENV_APP_PORT` ( prefix + "_" + key in ALL CAPS with _ delimiters)

    // Output the configuration values
    fmt.Printf("App Name: %s\n", appName)
    fmt.Printf("Namespace: %s\n", namespace)
    fmt.Printf("App Port: %d\n", appPort)

    // Create an instance of AppConfig
    var config AppConfig
    // Unmarshal the config file into the AppConfig struct
    err = viper.Unmarshal(&config)
    if err != nil {
        log.Fatalf("Unable to decode into struct, %v", err)
    }

    // Output the configuration values
    fmt.Printf("Config: %v\n", config)
}
Enter fullscreen mode Exit fullscreen mode

Using Environment Variables 🌐

To integrate environment variables dynamically, create a .env file with the following content:

export APP_NAME="MyCustomApp"
export ENV_NAMESPACE="go-viper"
export ENV_APP_PORT=9090
Enter fullscreen mode Exit fullscreen mode

Run the command to load the environment variables:

source .env
Enter fullscreen mode Exit fullscreen mode

In the code, Viper’s AutomaticEnv and SetEnvKeyReplacer methods allow you to map nested configuration keys like app.port to environment variables such as APP_PORT. Here’s how it works:

  1. Prefix with SetEnvPrefix: The line viper.SetEnvPrefix("env") ensures that all environment variable lookups are prefixed with ENV_. For example:
    • app.port becomes ENV_APP_PORT
    • namespace becomes ENV_NAMESPACE
  2. Key Replacements with SetEnvKeyReplacer: The SetEnvKeyReplacer(strings.NewReplacer(".", "_")) replaces . with _ in the key names, so nested keys like app.port can map directly to environment variables.

By combining these two methods, you can seamlessly override specific configuration values using environment variables.

🚀 Running the Example

Run the application using:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Expected Output:

App Name: MyCustomApp
Namespace: go-viper
App Port: 9090
Config: {{MyCustomApp 9090} go-viper John Doe}
Enter fullscreen mode Exit fullscreen mode

Best Practices 🌟

  • Use Environment Variables for Sensitive Data:Avoid storing secrets in config files. Use environment variables or secret management tools.
  • Set Default Values: Use viper.SetDefault("key", value) to ensure your application has sensible defaults.
  • Validate Configuration: After loading configurations, validate them to prevent runtime errors.
  • Keep Configuration Organized: Group related configurations together and use nested structs for clarity.

🔧 Using Viper with go-common Config Package

While Viper is a powerful tool for configuration management, in production-grade systems, it’s often beneficial to abstract and standardize how configuration is loaded, validated, and injected across services. That’s exactly what the go-common library provides via its config package.

Built on top of Viper, the config package simplifies and unifies your configuration loading process with:

  • Required or optional YAML file loading
  • Fallback default injection
  • Context-aware config propagation

Example: Using go-common Config in Your App

package main

import (
    "fmt"

    "github.com/kittipat1413/go-common/framework/config"
)

func main() {
    cfg := config.MustConfig(
        config.WithRequiredConfigPath("env.yaml"),
        config.WithDefaults(map[string]any{
            "SERVICE_NAME": "my-service",
            "SERVICE_PORT": ":8080",
            "ENV":          "development",
        }),
    )

    fmt.Println("=== Service Config ===")
    fmt.Println("Service Name:", cfg.GetString("SERVICE_NAME"))
    fmt.Println("Port:", cfg.GetString("SERVICE_PORT"))
    fmt.Println("Environment:", cfg.GetString("ENV"))
}
Enter fullscreen mode Exit fullscreen mode

Then in your env.yaml file, you can override any field:

SERVICE_NAME: "user-api"
SERVICE_PORT: ":9090"
ENV: "staging"
Enter fullscreen mode Exit fullscreen mode

🧪 Example in Practice
This approach is already used in real-world-style projects like my 🎟️ ticket-reservation system, which demonstrates how the go-common library can be used to manage configurations consistently across services.

📝 Conclusion

By leveraging Viper, you can simplify configuration management in your Go applications. Its flexibility to integrate multiple sources, dynamic environment variable support, and unmarshaling to structs make it an indispensable tool for developers.

Start using Viper in your next project and experience hassle-free configuration management. Happy coding! 🥂

Support My Work ☕

If you enjoy my work, consider buying me a coffee! Your support helps me keep creating valuable content and sharing knowledge. ☕

Buy Me A Coffee

Top comments (1)

Collapse
 
lucasdecamargo profile image
Lucas de Camargo

Great introduction to Viper! I've taken this a step further with a Field-Driven Configuration pattern that creates a single source of truth for all config metadata. It also integrates with Cobra to auto-generate CLI completions and documentation. I'm leaving the link here. Cheers!

Go Beyond Viper and Cobra: Declarative Field-Driven Configuration for Go Apps