Go Application Configuration: Best Practices and Tools

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Go (Golang) Application Configuration: From Environment Variables to Complex YAML Structures



Configuration is one of the key aspects of any production application. In Go (Golang), the ecosystem offers flexible and powerful tools for managing settings, adapted for different scenarios: from simple 12-factor applications to microservices with dozens of parameters. In this article, we will cover the main approaches, popular libraries, and best practices for configuring Go applications.



Why is separate configuration needed?

Hardcoding parameters is an anti-pattern. Configuration allows you to:

  • Change application behavior without recompilation;
  • Separate settings for different environments (dev/staging/prod);
  • Ensure security (secrets are not stored in the repository);
  • Simplify deployment in containers and cloud platforms.


Basic approach: environment variables

The Go standard library provides the os.Getenv function. This is a minimalistic but effective way for small projects, following the 12-factor app principles.



package main

import ( "fmt" "os")

type Config struct { Port string DBHost string DBUser string}

func loadConfig() Config { return Config{ Port: getEnv("APP_PORT", "8080"), DBHost: getEnv("DB_HOST", "localhost"), DBUser: getEnv("DB_USER", "admin"), }}

func getEnv(key, fallback string) string { if value, ok := os.LookupEnv(key); ok { return value } return fallback}

func main() { cfg := loadConfig() fmt.Printf("Server starting on port %s", cfg.Port)}


Viper library: the "Swiss Army knife" of configuration

Viper (from the creators of Cobra) is the most popular library for configuration in Go. It supports multiple formats, searching in several sources, and a hierarchy of settings.



Main features of Viper:

  • Reading from YAML, JSON, TOML, HCL, INI, envfile, Java properties;
  • Support for environment variables with custom prefixes;
  • Setting default values;
  • Dynamic configuration updates (live reload);
  • Integration with remote sources (etcd, Consul, Firebase).


Example of using Viper with YAML:

package main

import ( "fmt" "github.com/spf13/viper")

type AppConfig struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"`}

type ServerConfig struct { Port int `mapstructure:"port"` Host string `mapstructure:"host"`}

type DatabaseConfig struct { URL string `mapstructure:"url"` Pool int `mapstructure:"pool"`}

func main() { viper.SetConfigName("config") // file name without extension viper.SetConfigType("yaml") // format viper.AddConfigPath(".") // search path viper.AddConfigPath("./config/")

// Reading environment variables with prefix APP_ viper.AutomaticEnv() viper.SetEnvPrefix("APP")

if err := viper.ReadInConfig(); err != nil { panic(fmt.Errorf("fatal error config file: %s", err)) }

var cfg AppConfig if err := viper.Unmarshal(&cfg); err != nil { panic(fmt.Errorf("unable to decode into struct: %s", err)) }

fmt.Printf("Server: %s:%d", cfg.Server.Host, cfg.Server.Port) fmt.Printf("Database URL: %s", cfg.Database.URL)}


Example of a config.yaml file:

server:  port: 8080  host: "0.0.0.0"database:  url: "postgres://user:pass@localhost:5432/app"  pool: 10


envconfig library: simplicity and declarativeness

If you don't need complex formats, but only environment variables —

Recommendations