go 147 lines · 3 tabs

Load Go Configuration From Environment Variables With Typed Defaults

Shared by codesnips Aug 2026
3 tabs
package config

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

func getEnv(key, fallback string) string {
	if v, ok := os.LookupEnv(key); ok && v != "" {
		return v
	}
	return fallback
}

func required(key string) (string, error) {
	v, ok := os.LookupEnv(key)
	if !ok || v == "" {
		return "", fmt.Errorf("%s is required", key)
	}
	return v, nil
}

func getInt(key string, fallback int) (int, error) {
	raw, ok := os.LookupEnv(key)
	if !ok || raw == "" {
		return fallback, nil
	}
	n, err := strconv.Atoi(raw)
	if err != nil {
		return 0, fmt.Errorf("%s: invalid int %q", key, raw)
	}
	return n, nil
}

func getBool(key string, fallback bool) (bool, error) {
	raw, ok := os.LookupEnv(key)
	if !ok || raw == "" {
		return fallback, nil
	}
	b, err := strconv.ParseBool(raw)
	if err != nil {
		return false, fmt.Errorf("%s: invalid bool %q", key, raw)
	}
	return b, nil
}

func getDuration(key string, fallback time.Duration) (time.Duration, error) {
	raw, ok := os.LookupEnv(key)
	if !ok || raw == "" {
		return fallback, nil
	}
	d, err := time.ParseDuration(raw)
	if err != nil {
		return 0, fmt.Errorf("%s: invalid duration %q", key, raw)
	}
	return d, nil
}
3 files · go Explain with highlit

This snippet shows a small, dependency-free configuration loader for Go services that follows the twelve-factor convention of reading everything from the process environment. The design centers on a single Config struct that is populated once at startup, validated, and then treated as immutable for the lifetime of the process.

In config.go, the Load function builds a Config by delegating each field to a small set of typed helpers. getEnv returns a string with a fallback, while getInt, getBool, and getDuration wrap the standard library parsers (strconv.Atoi, strconv.ParseBool, and time.ParseDuration) so that a malformed value produces a clear error instead of a silent zero. Grouping the parsing this way keeps Load readable and makes the defaults visible in one place, which matters because the default is the behavior a developer gets when they forget to set a variable.

The helpers in env.go return an error rather than panicking, and Load accumulates any parse failures. The required helper enforces that a variable must be present, which is important for secrets like DATABASE_URL where a wrong default is more dangerous than a crash. The loader follows a fail-fast philosophy: it is better for a service to refuse to start with a precise message than to boot with a misconfigured timeout or a database pointing at localhost in production.

Validate performs cross-field checks that the parsers cannot express, such as ensuring Port is in range and that Env is one of the known values. Returning a joined error via errors.Join means every problem surfaces in a single startup log line rather than forcing the operator to fix one variable, restart, and discover the next.

The trade-off is a little boilerplate per field compared to reflection-based libraries, but the explicit approach keeps types obvious, avoids struct tags, and produces error messages that name the exact variable. main.go shows the intended usage: load, log a fatal error on failure, and pass the value object down. A common pitfall this avoids is scattering os.Getenv calls throughout the codebase, which makes defaults inconsistent and untestable; here all environment access is confined to the loader.


Related snips

Share this code

Here's the card — post it anywhere.

Load Go Configuration From Environment Variables With Typed Defaults — share card
Link copied