env.dev

Go Environment Variables: os.Getenv, godotenv & Viper

How to read, set, and manage environment variables in Go. Covers os.Getenv, os.LookupEnv, godotenv, Viper, envconfig, build-time variables, and testing.

Last updated:

Go reads environment variables through the os package in the standard library, but production applications typically layer on libraries like godotenv, viper, or envconfig for file loading, struct binding, and validation. Every environment variable is a string at the OS level, so converting to the correct Go type is your responsibility unless you use a library that handles it. This guide covers the standard library, the most popular third-party packages, build-time injection with linker flags, and testing patterns.

How do os.Getenv() and os.LookupEnv() differ?

The os package provides two ways to read environment variables. os.Getenv() returns the value or an empty string if the variable is not set. os.LookupEnv() returns both the value and a boolean indicating whether the variable exists, letting you distinguish between unset and set-but-empty.

go
package main

import (
	"fmt"
	"os"
)

func main() {
	// Returns "" if DATABASE_URL is not set — no way to tell if it was empty or missing
	dbURL := os.Getenv("DATABASE_URL")
	fmt.Println(dbURL)

	// Returns the value and a boolean — distinguishes unset from empty
	val, exists := os.LookupEnv("DATABASE_URL")
	if !exists {
		fmt.Println("DATABASE_URL is not set")
	} else {
		fmt.Println("DATABASE_URL =", val)
	}
}

Use os.LookupEnv() when you need to differentiate between a missing variable and one explicitly set to an empty string. Use os.Getenv() when an empty default is acceptable.

How do you set and unset environment variables?

os.Setenv() sets a variable for the current process and any child processes. os.Unsetenv() removes it. Both return an error on failure.

go
// Set a variable for the current process
if err := os.Setenv("APP_MODE", "debug"); err != nil {
	log.Fatal(err)
}

// Remove a variable
if err := os.Unsetenv("APP_MODE"); err != nil {
	log.Fatal(err)
}

// List all environment variables
for _, env := range os.Environ() {
	fmt.Println(env) // prints KEY=VALUE pairs
}

How do you load a .env file with godotenv?

The github.com/joho/godotenv package reads key-value pairs from a .env file and injects them into the process environment. Install it with go get github.com/joho/godotenv.

go
package main

import (
	"log"
	"os"

	"github.com/joho/godotenv"
)

func main() {
	// Load .env from the current directory
	if err := godotenv.Load(); err != nil {
		log.Println("No .env file found")
	}

	// Load a specific file
	if err := godotenv.Load("/app/config/.env"); err != nil {
		log.Fatal(err)
	}

	// Load multiple files — later files do NOT override earlier values
	if err := godotenv.Load(".env", ".env.local"); err != nil {
		log.Fatal(err)
	}

	// Overload replaces existing values (like override mode)
	if err := godotenv.Overload(".env.local"); err != nil {
		log.Fatal(err)
	}

	// Read variables normally after loading
	dbURL := os.Getenv("DATABASE_URL")
	log.Println("DB:", dbURL)
}

Call godotenv.Load() before reading any variables, typically at the start of main(). See the .env guide for file syntax details.

How does Viper handle configuration and env vars?

github.com/spf13/viper is a full configuration management library that reads from environment variables, config files (JSON, YAML, TOML), remote key-value stores, and command flags. It can bind env vars automatically using a prefix.

go
package main

import (
	"fmt"
	"log"

	"github.com/spf13/viper"
)

func main() {
	// Automatically prefix all env lookups with APP_
	viper.SetEnvPrefix("APP")

	// Bind specific keys to env vars: APP_PORT, APP_DEBUG
	viper.BindEnv("port")
	viper.BindEnv("debug")

	// Or bind all env vars automatically
	viper.AutomaticEnv()

	// Set defaults
	viper.SetDefault("port", 8080)
	viper.SetDefault("debug", false)

	// Read a config file alongside env vars (env takes priority)
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")
	if err := viper.ReadInConfig(); err != nil {
		log.Println("No config file found, using env and defaults")
	}

	fmt.Println("Port:", viper.GetInt("port"))
	fmt.Println("Debug:", viper.GetBool("debug"))
}

How do you bind env vars to structs with envconfig?

github.com/kelseyhightower/envconfig maps environment variables directly to struct fields using a prefix and struct tags. It handles type conversion automatically.

go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/kelseyhightower/envconfig"
)

type Config struct {
	Port           int           `envconfig:"PORT" default:"8080"`
	Debug          bool          `envconfig:"DEBUG" default:"false"`
	DatabaseURL    string        `envconfig:"DATABASE_URL" required:"true"`
	AllowedOrigins []string      `envconfig:"ALLOWED_ORIGINS" default:"localhost"`
	ReadTimeout    time.Duration `envconfig:"READ_TIMEOUT" default:"5s"`
}

func main() {
	var cfg Config
	// "APP" prefix means it reads APP_PORT, APP_DEBUG, APP_DATABASE_URL, etc.
	if err := envconfig.Process("APP", &cfg); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Port: %d, Debug: %v, DB: %s\n", cfg.Port, cfg.Debug, cfg.DatabaseURL)
	fmt.Printf("Origins: %v, Timeout: %s\n", cfg.AllowedOrigins, cfg.ReadTimeout)
}

The required:"true" tag causes envconfig.Process() to return an error if the variable is not set. Slices are split on commas by default, and time.Duration values are parsed natively.

How do you inject variables at build time with ldflags?

Go's linker flags (-ldflags) let you set string variables at compile time without environment variables at runtime. This is commonly used for version strings and build metadata.

go
package main

import "fmt"

// These are set at build time via -ldflags
var (
	Version   = "dev"
	CommitSHA = "unknown"
	BuildTime = "unknown"
)

func main() {
	fmt.Printf("Version: %s, Commit: %s, Built: %s\n", Version, CommitSHA, BuildTime)
}
go
// Build command — sets the variables at link time
// go build -ldflags "-X main.Version=1.2.3 -X main.CommitSHA=$(git rev-parse HEAD) -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o myapp .

The -X flag sets a string variable in the specified package. The variable must be of type string and must be a package-level variable.

What is the config struct validation pattern?

A common Go pattern is to centralize configuration in a struct, load it once at startup, and validate all values before the application starts serving traffic.

go
package config

import (
	"errors"
	"os"
	"strconv"
)

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

func Load() (*Config, error) {
	port, err := strconv.Atoi(getEnvOrDefault("PORT", "8080"))
	if err != nil {
		return nil, errors.New("PORT must be a valid integer")
	}

	cfg := &Config{
		DatabaseURL: os.Getenv("DATABASE_URL"),
		Port:        port,
		Debug:       os.Getenv("DEBUG") == "true",
		SecretKey:   os.Getenv("SECRET_KEY"),
	}

	if err := cfg.validate(); err != nil {
		return nil, err
	}
	return cfg, nil
}

func (c *Config) validate() error {
	if c.DatabaseURL == "" {
		return errors.New("DATABASE_URL is required")
	}
	if c.SecretKey == "" {
		return errors.New("SECRET_KEY is required")
	}
	if c.Port < 1 || c.Port > 65535 {
		return errors.New("PORT must be between 1 and 65535")
	}
	return nil
}

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

How do you test with environment variables in Go?

Go 1.17 introduced t.Setenv(), which sets an environment variable for the duration of a test and restores the original value automatically when the test completes.

go
package config_test

import (
	"os"
	"testing"
)

func TestLoadConfig(t *testing.T) {
	// t.Setenv sets the variable and restores it after the test
	t.Setenv("DATABASE_URL", "postgres://localhost/testdb")
	t.Setenv("SECRET_KEY", "test-secret")
	t.Setenv("PORT", "3000")

	cfg, err := Load()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if cfg.Port != 3000 {
		t.Errorf("expected port 3000, got %d", cfg.Port)
	}
}

func TestMissingRequiredVar(t *testing.T) {
	// Unset the variable to test validation
	t.Setenv("DATABASE_URL", "")
	t.Setenv("SECRET_KEY", "test-secret")

	_, err := Load()
	if err == nil {
		t.Fatal("expected error for missing DATABASE_URL")
	}
}

func TestGetenvFallback(t *testing.T) {
	// Verify LookupEnv-based fallback works
	os.Unsetenv("MY_VAR")
	val := getEnvOrDefault("MY_VAR", "default-value")
	if val != "default-value" {
		t.Errorf("expected default-value, got %s", val)
	}
}

t.Setenv() automatically calls t.Cleanup() to restore the previous value, so you never leak state between tests. Note that t.Setenv() cannot be used in parallel tests — calling it from a test that uses t.Parallel() will panic.

References

Check your .env files for syntax errors with the env validator, read the .env guide for full dotenv syntax, or skim the Go cheat sheet for syntax at a glance.

Was this helpful?

Read next

Next.js Environment Variables: Complete Guide

How Next.js handles environment variables: .env file load order, NEXT_PUBLIC_ prefix, server vs client access, and common production errors.

Continue →

Frequently Asked Questions

How do I read an environment variable in Go?

Use os.Getenv("KEY") which returns the value or an empty string if not set. Use os.LookupEnv("KEY") when you need to distinguish between an empty value and a missing variable — it returns both the value and a boolean.

How do I use .env files in Go?

Use the godotenv package: import "github.com/joho/godotenv" and call godotenv.Load() at the start of main(). It reads .env and sets values in the environment. Alternatively, use Viper which supports .env files alongside YAML, JSON, and TOML.

How do I validate my .env file for syntax errors?

Use the env validator at /tools/env-validator to check for quoting issues, missing values, and invalid syntax before your application tries to parse the file. This catches mistakes that godotenv would silently ignore or misparse.

Stay up to date

Get notified about new guides, tools, and cheatsheets.