envconfig
Concept

Problem Being Solved

What problem this product addresses


Overview

Most Go applications need configuration that changes between environments — database URLs, ports, feature flags, and secrets that differ between development, staging, and production. Managing that configuration without envconfig typically means writing repetitive boilerplate: reading environment variables one by one, converting types manually, and handling missing or malformed values yourself. This page explains the core problem envconfig solves and why environment variables, paired with automatic struct population, are a practical solution for configuring Go applications.


Content

The configuration problem every Go app faces

When you build a Go application, you eventually need to answer a simple question: how does the app know where the database lives, which port to listen on, or whether debug logging is enabled? The answer almost always involves configuration — and where that configuration comes from matters a great deal.

Hard-coding values works for a first draft, but breaks the moment you deploy to a second environment. Configuration files are more flexible, but introduce questions about file format, file location, and how to manage secrets safely. Environment variables avoid most of these problems: they are supported on every operating system and hosting platform, they integrate naturally with container runtimes and CI/CD pipelines, and they keep secrets out of your source code.

Why raw environment variable handling is painful

Go's standard library gives you os.Getenv to read environment variables, but using it directly quickly becomes tedious. Consider what you need to do for a single integer field:

  1. Call os.Getenv("MYAPP_PORT") to get a string.
  2. Check whether the string is empty and decide whether that is an error or an acceptable default.
  3. Call strconv.Atoi to convert the string to an integer.
  4. Handle the conversion error.
  5. Repeat for every field in your configuration.

For a struct with a dozen fields of mixed types — booleans, durations, slices, maps — you end up with dozens of lines of near-identical code. That code is easy to get wrong, hard to test in isolation, and tedious to maintain as your configuration grows.

What envconfig does instead

envconfig lets you describe your configuration once as a plain Go struct, then calls a single function to populate every field from the corresponding environment variables automatically. Type conversion, error handling, default values, and required-field validation all happen inside the library rather than in your application code.

The mapping from struct fields to environment variables follows a predictable convention: a prefix you choose (representing your application name) combined with the uppercased field name. A field named Port in an app prefixed myapp is populated from MYAPP_PORT. You can override this convention per field using struct tags when the default does not suit your naming scheme.

The convention reduces cognitive load

Because the mapping is deterministic, anyone reading your struct can immediately know which environment variable controls each field — no separate documentation needed, no runtime surprises. The struct is the documentation.

Struct tags give you escape hatches

When you need more control, struct tags let you:

  • Rename a variable with envconfig:"MY_CUSTOM_NAME" to match an existing naming convention.
  • Set a default with default:"value" so the application works out of the box without every variable being set.
  • Enforce presence with required:"true" so the application fails fast at startup rather than misbehaving later with a zero value.
  • Split CamelCase into underscores automatically with split_words:"true" for friendlier variable names.
  • Ignore a field entirely with ignored:"true" when a struct field should never be populated from the environment.

Rich type support removes conversion code

envconfig handles the conversions you would otherwise write by hand: integers, floats, booleans, time.Duration, slices (comma-separated values), and maps (colon-separated key-value pairs). For types that do not fit a built-in pattern, you can implement a Decode(string) error method on the type and envconfig will call it, giving you full control without giving up the convention-based approach.

The result

Instead of dozens of lines of os.Getenv, type-conversion, and error-handling code scattered across your application, you get a single struct definition and a single function call. Your application's configuration surface becomes visible, validated, and typed from the very first line of main.


Examples

The problem without envconfig

This is the kind of boilerplate you would write to load just three configuration values by hand:

package main

import (
    "log"
    "os"
    "strconv"
    "time"
)

func main() {
    // Read and validate port
    portStr := os.Getenv("MYAPP_PORT")
    if portStr == "" {
        log.Fatal("MYAPP_PORT is required")
    }
    port, err := strconv.Atoi(portStr)
    if err != nil {
        log.Fatalf("MYAPP_PORT is not a valid integer: %v", err)
    }

    // Read and validate timeout
    timeoutStr := os.Getenv("MYAPP_TIMEOUT")
    if timeoutStr == "" {
        log.Fatal("MYAPP_TIMEOUT is required")
    }
    timeout, err := time.ParseDuration(timeoutStr)
    if err != nil {
        log.Fatalf("MYAPP_TIMEOUT is not a valid duration: %v", err)
    }

    // Read optional debug flag
    debug := false
    debugStr := os.Getenv("MYAPP_DEBUG")
    if debugStr != "" {
        debug, err = strconv.ParseBool(debugStr)
        if err != nil {
            log.Fatalf("MYAPP_DEBUG is not a valid bool: %v", err)
        }
    }

    _ = port
    _ = timeout
    _ = debug
}

That is roughly 30 lines for three fields. A real application configuration struct with 10–15 fields produces hundreds of lines of similar code.


The same configuration with envconfig

Set the environment variables:

export MYAPP_DEBUG=false
export MYAPP_PORT=8080
export MYAPP_USER=Kelsey
export MYAPP_RATE="0.5"
export MYAPP_TIMEOUT="3m"
export MYAPP_USERS="rob,ken,robert"
export MYAPP_COLORCODES="red:1,green:2,blue:3"

Define a struct and call envconfig.Process:

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/kelseyhightower/envconfig"
)

type Specification struct {
    Debug      bool
    Port       int
    User       string
    Users      []string
    Rate       float32
    Timeout    time.Duration
    ColorCodes map[string]int
}

func main() {
    var s Specification
    err := envconfig.Process("myapp", &s)
    if err != nil {
        log.Fatal(err.Error())
    }
    format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nTimeout: %s\n"
    fmt.Printf(format, s.Debug, s.Port, s.User, s.Rate, s.Timeout)

    fmt.Println("Users:")
    for _, u := range s.Users {
        fmt.Printf("  %s\n", u)
    }

    fmt.Println("Color codes:")
    for k, v := range s.ColorCodes {
        fmt.Printf("  %s: %d\n", k, v)
    }
}

Expected output:

Debug: false
Port: 8080
User: Kelsey
Rate: 0.500000
Timeout: 3m0s
Users:
  rob
  ken
  robert
Color codes:
  red: 1
  green: 2
  blue: 3

All type conversion, slice parsing, and map parsing happened automatically. The struct definition is now the complete specification of what your application reads from the environment.


Related concepts
  • Twelve-Factor App configuration — The convention of storing configuration in environment variables comes from the Twelve-Factor App methodology, which explains why environment variables are preferable to config files for deployed services.
  • Go struct tags — envconfig relies heavily on Go struct tags for customisation. The Go specification on struct tags and the reflect package documentation explain how tags work at the language level.
  • envconfig GoDoc reference — The godoc page for envconfig lists every supported type, tag, and interface in full detail.
  • encoding.TextUnmarshaler and encoding.BinaryUnmarshaler — If you work with types that already implement these standard library interfaces, envconfig will use them automatically. See the encoding package documentation for details.
  • flag.Value interface — envconfig also honours the Set(string) error method from the flag package, making it compatible with types already written for flag-based configuration.