Last modified: Jan 23, 2026 By Alexander Williams

Using Environment Variables in Go

Environment variables are key for configuration. They keep secrets out of your code. This makes your Go applications secure and portable.

You can set different values for development, testing, and production. This guide shows you how to use them effectively in Go.

Why Use Environment Variables?

Hard-coding configuration is a bad practice. It exposes sensitive data like API keys and database passwords. Environment variables solve this problem.

They are external to your application binary. You can change them without recompiling your code. This is essential for the twelve-factor app methodology.

Use them for database URLs, API endpoints, and feature flags. It keeps your code clean and your data safe.

The os Package Basics

Go's standard library provides the os package. It has functions to work with environment variables. The main functions are os.Getenv, os.Setenv, and os.LookupEnv.

Getting a Variable with os.Getenv

Use os.Getenv to read a variable. It takes the variable name as a string argument. It returns the value as a string.


package main

import (
    "fmt"
    "os"
)

func main() {
    // Get the value of the "HOME" environment variable
    homeDir := os.Getenv("HOME")
    fmt.Printf("Your home directory is: %s\n", homeDir)

    // Try to get a variable that might not be set
    apiKey := os.Getenv("API_KEY")
    if apiKey == "" {
        fmt.Println("API_KEY is not set.")
    } else {
        fmt.Printf("API Key is set (length: %d)\n", len(apiKey))
    }
}

Your home directory is: /home/user
API_KEY is not set.

Notice that os.Getenv returns an empty string if the variable is not found. You must check for this empty value. This is a common source of bugs.

Checking for Variables with os.LookupEnv

The os.LookupEnv function is safer. It returns the value and a boolean indicating if the variable was set. This prevents confusion with empty values.


package main

import (
    "fmt"
    "os"
)

func main() {
    // Use os.LookupEnv to safely check for a variable
    port, exists := os.LookupEnv("SERVER_PORT")
    if !exists {
        fmt.Println("SERVER_PORT is not set. Using default 8080.")
        port = "8080"
    } else {
        fmt.Printf("Server will run on port: %s\n", port)
    }

    // This is clearer than checking for an empty string
    dbHost, found := os.LookupEnv("DB_HOST")
    if found {
        fmt.Println("Database host is configured.")
    }
}

SERVER_PORT is not set. Using default 8080.
Database host is configured.

Using os.LookupEnv is a best practice. It makes your intention clear and your code more robust.

Setting Variables with os.Setenv

You can set environment variables within your program using os.Setenv. This is useful for testing. The change only affects the current process and its children.


package main

import (
    "fmt"
    "os"
)

func main() {
    // Set an environment variable for this process
    os.Setenv("APP_MODE", "development")

    // Now retrieve it
    mode := os.Getenv("APP_MODE")
    fmt.Printf("Application mode: %s\n", mode)

    // List all environment variables (for demonstration)
    fmt.Println("\nAll environment variables:")
    for _, env := range os.Environ() {
        fmt.Println(env)
    }
}

Application mode: development

All environment variables:
PATH=/usr/local/sbin:/usr/local/bin...
APP_MODE=development
...

Remember, variables set this way are temporary. They are lost when the program exits. For permanent changes, set them in your shell profile or system.

Loading Variables from a .env File

Managing many variables in the shell is messy. A popular solution is the .env file. It stores variables in a key-value format. You load them at application startup.

Go's standard library does not have a .env loader. You need a third-party package. The github.com/joho/godotenv package is the most popular.

Using the godotenv Package

First, install the package.


go get github.com/joho/godotenv

Then, create a file named .env in your project root.


# .env file
DB_HOST=localhost
DB_PORT=5432
DB_USER=myuser
DB_PASSWORD=supersecret
DEBUG=true

Now, load this file in your Go code.


package main

import (
    "fmt"
    "log"
    "os"

    "github.com/joho/godotenv"
)

func main() {
    // Load environment variables from .env file
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    // Access variables as usual
    dbHost := os.Getenv("DB_HOST")
    dbPort := os.Getenv("DB_PORT")
    debug := os.Getenv("DEBUG")

    fmt.Printf("Database Host: %s\n", dbHost)
    fmt.Printf("Database Port: %s\n", dbPort)
    fmt.Printf("Debug Mode: %s\n", debug)
}

Database Host: localhost
Database Port: 5432
Debug Mode: true

The godotenv.Load() function reads the .env file. It sets the variables into the program's environment. You can then use os.Getenv to access them.

Never commit your .env file to version control. Add it to your .gitignore file. Share a .env.example file with placeholder values instead.

Structuring Configuration with Variables

Scattering os.Getenv calls throughout your code is not ideal. A better approach is to load all configuration at startup. Store it in a configuration struct.

This provides type safety and a single source of truth. It relates to concepts like Using Variables in Go Structs.


package main

import (
    "fmt"
    "log"
    "os"
    "strconv"

    "github.com/joho/godotenv"
)

// Config holds all application configuration
type Config struct {
    DBHost     string
    DBPort     int
    DBUser     string
    DBPassword string
    Debug      bool
    MaxWorkers int
}

func LoadConfig() (*Config, error) {
    // Load .env file
    err := godotenv.Load()
    if err != nil {
        log.Println("Warning: No .env file found, using system environment")
    }

    cfg := &Config{}

    // Load string values
    cfg.DBHost = getEnvOrDefault("DB_HOST", "localhost")
    cfg.DBUser = getEnvOrDefault("DB_USER", "postgres")
    cfg.DBPassword = os.Getenv("DB_PASSWORD") // Required, no default

    // Load and convert integer values
    cfg.DBPort = getEnvAsInt("DB_PORT", 5432)
    cfg.MaxWorkers = getEnvAsInt("MAX_WORKERS", 10)

    // Load and convert boolean values
    cfg.Debug = getEnvAsBool("DEBUG", false)

    // Validation
    if cfg.DBPassword == "" {
        return nil, fmt.Errorf("DB_PASSWORD environment variable is required")
    }

    return cfg, nil
}

// Helper function to get string or default
func getEnvOrDefault(key, defaultValue string) string {
    if value, exists := os.LookupEnv(key); exists {
        return value
    }
    return defaultValue
}

// Helper function to get integer or default
func getEnvAsInt(key string, defaultValue int) int {
    valueStr := os.Getenv(key)
    if valueStr == "" {
        return defaultValue
    }
    value, err := strconv.Atoi(valueStr)
    if err != nil {
        log.Printf("Invalid integer for %s: %s. Using default: %d", key, valueStr, defaultValue)
        return defaultValue
    }
    return value
}

// Helper function to get boolean or default
func getEnvAsBool(key string, defaultValue bool) bool {
    valueStr := os.Getenv(key)
    if valueStr == "" {
        return defaultValue
    }
    boolValue, err := strconv.ParseBool(valueStr)
    if err != nil {
        log.Printf("Invalid boolean for %s: %s. Using default: %v", key, valueStr, defaultValue)
        return defaultValue
    }
    return boolValue
}

func main() {
    cfg, err := LoadConfig()
    if err != nil {
        log.Fatal("Failed to load config:", err)
    }

    fmt.Printf("Config Loaded:\n")
    fmt.Printf("  DB Host: %s\n", cfg.DBHost)
    fmt.Printf("  DB Port: %d\n", cfg.DBPort)
    fmt.Printf("  DB User: %s\n", cfg.DBUser)
    fmt.Printf("  Debug Mode: %v\n", cfg.Debug)
    fmt.Printf("  Max Workers: %d\n", cfg.MaxWorkers)
}

Config Loaded:
  DB Host: localhost
  DB Port: 5432
  DB User: myuser
  Debug Mode: true
  Max Workers: 10

This pattern is powerful. It centralizes configuration logic. It handles type conversion and default values. It validates critical settings before the app starts.

Understanding Go Constants vs Variables can help you decide what should be an environment variable versus a fixed constant in your config struct.

Best Practices and Security

Using environment variables correctly is crucial for security.

Never log environment variables. Especially secrets like passwords and API keys. Your logs might be exposed.

Use different .env files for different environments. For example, .env.development and .env.production. Load the correct one based on a master variable like GO_ENV.

For sensitive production secrets, consider a vault service. Tools like HashiCorp Vault or cloud-specific secret managers are more secure. They provide access control and audit logs.

Be mindful of Go Variable Shadowing Mistakes when writing helper functions that interact with environment variables and local variables.

Conclusion

Environment variables are a cornerstone of modern Go application configuration. The os package provides the basic tools. The godotenv library simplifies development.

The key is to use them thoughtfully. Load them early. Validate them. Store them in a well-defined struct. This leads to applications that are secure, configurable, and easy to deploy across any environment.

Start by replacing hard-coded values in your next project. Use os.LookupEnv for safety. Adopt a structured configuration pattern. Your future self and your team will thank you.