Question

How to Handle Configuration in Go

goconfiguration-files

Question

What is the preferred way to manage configuration parameters in a Go program—the kind of settings that might be stored in properties files or INI files in other environments?

Short Answer

By the end of this page, you will understand the common ways to handle configuration in Go, when to use command-line flags, environment variables, or configuration files, and how to structure configuration cleanly in real projects.

Concept

In Go, configuration usually means the values your program needs in order to run correctly, but which should not be hard-coded into the source code.

Examples include:

  • Server port numbers
  • Database connection strings
  • API keys
  • Feature flags
  • File paths
  • Timeouts

A common Go approach is to load configuration into a struct. The values may come from one or more sources:

  • Command-line flags using the standard flag package
  • Environment variables using os.Getenv or os.LookupEnv
  • Configuration files such as JSON, YAML, TOML, or INI, usually parsed with a package

Why this matters:

  • It keeps code portable across development, testing, and production
  • It avoids hard-coding secrets or machine-specific settings
  • It makes applications easier to deploy and maintain
  • It centralizes setup logic in one place

In real Go programs, there is no single mandatory format for configuration. The preferred approach depends on the app:

  • Small CLI tool: flags may be enough
  • Web service: environment variables are very common
  • Larger app: a config struct loaded from file and/or environment variables is common

The key idea is not the file format itself. The key idea is to define configuration clearly, load it early, validate it, and pass it into the rest of the program.

Mental Model

Think of configuration as the settings panel for your program.

Your code is the machine. Configuration is the set of switches and dials that control how the machine runs in different environments.

  • In development, the dial might point to a local database
  • In production, the same dial points to a production database
  • The machine stays the same, but the settings change

In Go, a struct is like a labeled control board:

type Config struct {
    Port string
    DBURL string
    Debug bool
}

Each field is one setting. Your job is to fill in that control board from flags, environment variables, or files before the rest of the app starts.

Take Quiz

Syntax and Examples

A common beginner-friendly pattern is:

  1. Define a config struct
  2. Load values into it
  3. Validate required fields
  4. Use the config in main

Example: configuration from environment variables

package main

import (
    "fmt"
    "os"
)

type Config struct {
    Port  string
    DBURL string
}

func loadConfig() Config {
    cfg := Config{
        Port:  os.Getenv("PORT"),
        DBURL: os.Getenv("DATABASE_URL"),
    }

    if cfg.Port == "" {
        cfg.Port = "8080"
    }

    return cfg
}

func main() {
    cfg := loadConfig()
    fmt.Println("Port:", cfg.Port)
    fmt.Println("Database URL:", cfg.DBURL)
}

What this example shows

  • Config stores all settings in one place
  • os.Getenv reads environment variables
  • A default value is used for Port if it is not set
  • The rest of the program can use cfg instead of calling os.Getenv everywhere

Example: configuration from command-line flags

package main

import (
    "flag"
    "fmt"
)

func main() {
    port := flag.String("port", "8080", "server port")
    debug := flag.Bool("debug", false, "enable debug mode")
    flag.Parse()

    fmt.Println("Port:", *port)
    fmt.Println("Debug:", *debug)
}

This is useful for CLI programs or tools where users expect options like:

myapp -port=9000 -debug=true

Example: configuration from a JSON file

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Config struct {
    Port  string `json:"port"`
    DBURL string `json:"db_url"`
}

func loadConfig(filename string) (Config, error) {
    var cfg Config

    data, err := os.ReadFile(filename)
    if err != nil {
        return cfg, err
    }

    err = json.Unmarshal(data, &cfg)
    return cfg, err
}

func main() {
    cfg, err := loadConfig("config.json")
    if err != nil {
        fmt.Println("Error loading config:", err)
        return
    }

    fmt.Println(cfg)
}

If config.json contains:

{
  "port": "8080",
  "db_url": "postgres://localhost/mydb"
}

then the struct gets filled automatically.

Step by Step Execution

Consider this example:

package main

import (
    "fmt"
    "os"
)

type Config struct {
    Port string
}

func loadConfig() Config {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    return Config{Port: port}
}

func main() {
    cfg := loadConfig()
    fmt.Println("Starting server on port", cfg.Port)
}

Step-by-step

  1. The program starts in main().
  2. main() calls loadConfig().
  3. Inside loadConfig(), os.Getenv("PORT") checks whether the PORT environment variable exists.
  4. If PORT is not set, Go returns an empty string.
  5. The if port == "" check detects that case.
  6. The code sets a default value: port = "8080".
  7. A Config struct is created with Port: port.
  8. That struct is returned to main().
  9. main() prints the port value.

If PORT is set

If you run:

PORT=9000 go run main.go

then step 4 changes:

  • os.Getenv("PORT") returns "9000"
  • The default is not used
  • The program prints Starting server on port 9000

This pattern is simple and easy to trace, which is why it is common in beginner and production code alike.

Real World Use Cases

Configuration appears in almost every Go application.

Web servers

  • HTTP port
  • TLS certificate paths
  • Allowed origins
  • Request timeout values

Database-backed apps

  • Database host and port
  • Username and password
  • Connection pool settings
  • Migration directory path

CLI tools

  • Input and output file paths
  • Verbose mode
  • Dry-run mode
  • Target environment

Background workers

  • Queue URL
  • Retry count
  • Poll interval
  • Logging level

API integrations

  • API base URL
  • API token
  • Request timeout
  • Rate-limit settings

In all of these cases, hard-coding values would make deployment difficult. Configuration allows the same compiled Go program to run in multiple environments with different settings.

Take Quiz

Real Codebase Usage

In real Go codebases, developers usually follow a few practical patterns.

Central config struct

A single struct holds all configuration:

type Config struct {
    Port        string
    DatabaseURL string
    Debug       bool
    TimeoutSecs int
}

This keeps setup organized and avoids scattered config lookups.

Load early in main

A common pattern is:

func main() {
    cfg, err := LoadConfig()
    if err != nil {
        panic(err)
    }

    app := NewApp(cfg)
    app.Run()
}

This ensures the app fails fast if configuration is invalid.

Validation

Required values should be checked immediately:

if cfg.DatabaseURL == "" {
    return errors.New("DATABASE_URL is required")
}

This is better than letting the program fail later in a less clear place.

Defaults

Defaults are often applied for optional settings:

  • Port defaults to 8080
  • Log level defaults to info
  • Timeout defaults to 30

Environment-first for deployed services

Many Go services use environment variables in containers and cloud platforms because they work well with Docker, Kubernetes, and CI/CD systems.

Layered configuration

A common approach is to combine sources in this order:

  1. Default values
  2. Config file
  3. Environment variables
  4. Command-line flags

Later sources override earlier ones.

This gives flexibility while keeping behavior predictable.

Common Mistakes

Here are common beginner mistakes when handling configuration in Go.

1. Scattering config reads all over the code

Broken approach:

func connectDB() {
    url := os.Getenv("DATABASE_URL")
    // use url
}

func startServer() {
    port := os.Getenv("PORT")
    // use port
}

Why this is a problem:

  • Harder to test
  • Harder to find all config requirements
  • Repeated logic for defaults and validation

Better approach:

  • Load everything once into a Config struct
  • Pass the struct where needed

2. Not validating required values

Broken code:

cfg := Config{
    DBURL: os.Getenv("DATABASE_URL"),
}

If DATABASE_URL is missing, the app may fail much later.

Better:

if cfg.DBURL == "" {
    return fmt.Errorf("DATABASE_URL is required")
}

3. Hard-coding secrets

Broken code:

apiKey := "my-secret-key"

Why this is bad:

  • Unsafe
  • Hard to rotate
  • Different environments need different values

Use environment variables or a secure secret system instead.

4. Using strings for everything

If a value is numeric or boolean, parse it into the correct type.

Example:

package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    raw := os.Getenv("DEBUG")
    debug, err := strconv.ParseBool(raw)
    if err != nil && raw != "" {
        fmt.Println("invalid DEBUG value")
        return
    }

    fmt.Println(debug)
}

5. Ignoring errors from config file parsing

Broken code:

data, _ := os.ReadFile("config.json")
json.Unmarshal(data, &cfg)

Always check errors so the app can fail clearly.

6. Making configuration too magical

Avoid hidden rules and unclear precedence. If your app reads from flags, environment variables, and files, document which source wins.

Comparisons

Here is a practical comparison of common configuration sources in Go.

ApproachBest forProsCons
Command-line flagsCLI tools, local runsBuilt into Go, easy to use, explicitLess convenient for large config sets
Environment variablesServices, containers, deploymentWorks well in cloud environments, easy to overrideAll values start as strings
JSON/YAML/TOML/INI filesLarger structured configGood for many related settings, easier to reviewRequires file management and parsing
Hard-coded constantsRarely for true configSimple for fixed valuesNot flexible, poor for deployment

Flags vs environment variables

FeatureFlagsEnvironment Variables
Good for CLI usageYesSometimes
Good for container deploymentLess commonYes
Built into standard libraryYesYes
Easy for secretsNoBetter, though external secret stores are often best

File config vs environment variables

  • File config is useful when many related settings need to be grouped together.
  • Environment variables are useful when deployment platforms inject settings automatically.
  • Many real applications support both.
Take Quiz

Cheat Sheet

// Environment variable
value := os.Getenv("PORT")
value, ok := os.LookupEnv("PORT")

// Command-line flag
port := flag.String("port", "8080", "server port")
flag.Parse()
fmt.Println(*port)

// JSON config file
var cfg Config
data, err := os.ReadFile("config.json")
if err != nil {
    return err
}
err = json.Unmarshal(data, &cfg)

Good practices

  • Define a Config struct
  • Load config once, near program startup
  • Apply defaults for optional values
  • Validate required values
  • Parse values into correct types
  • Pass config into functions or components

Common precedence order

  1. Defaults
  2. Config file
  3. Environment variables
  4. Flags

Useful standard packages

  • flag for command-line arguments
  • os for environment variables and file reading
  • encoding/json for JSON config files
  • strconv for converting strings to bool, int, and more

Important rule

Do not spread configuration lookups throughout your code. Centralize them.

FAQ

What is the most common way to handle configuration in Go?

A common approach is to load settings into a struct from environment variables, flags, or a config file, then validate and use that struct throughout the app.

Should I use environment variables or config files in Go?

Use environment variables when deploying services or containers. Use config files when you have many structured settings. Many projects support both.

Does Go have a built-in INI or properties file parser?

Not in the standard library. Go includes packages for flags, environment variables, and JSON support, but other file formats usually require external packages.

Where should I load configuration in a Go app?

Usually near the start of main(), before creating the rest of the application.

Should configuration be stored in global variables?

Usually no. It is cleaner and easier to test when configuration is stored in a struct and passed where needed.

How do I set default values in Go configuration?

You can assign defaults when a value is missing, such as using 8080 when PORT is empty.

How do I validate required configuration values?

Check them after loading and return an error if a required field is missing or invalid.

Is it okay to store secrets in config files?

Usually avoid committing secrets to source-controlled config files. Environment variables or dedicated secret managers are safer choices.

Take Quiz

Mini Project

Description

Build a small Go application that loads configuration for a web service from environment variables. This demonstrates a practical, common Go pattern: collect settings into a struct, apply defaults, validate required values, and print the final startup settings.

Goal

Create a Go program that reads app settings from the environment, applies defaults, validates required fields, and starts with a clear configuration summary.

Requirements

  • Define a Config struct with fields for port, app name, debug mode, and database URL.
  • Load values from environment variables.
  • Use default values for optional settings such as port and app name.
  • Validate that the database URL is provided.
  • Parse the debug value as a boolean.
  • Print the loaded configuration or an error message.
Take Quiz

Keep learning

Related questions

View more