Problem Being Solved
What problem this product addresses
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.
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:
- Call
os.Getenv("MYAPP_PORT")to get a string. - Check whether the string is empty and decide whether that is an error or an acceptable default.
- Call
strconv.Atoito convert the string to an integer. - Handle the conversion error.
- 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.
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.
- 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.TextUnmarshalerandencoding.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.Valueinterface — envconfig also honours theSet(string) errormethod from the flag package, making it compatible with types already written for flag-based configuration.