Environment Variables in Go
Learn how to work with environment variables in Go applications for configuration management and deployment.
Why Use Environment Variables?
Environment variables are essential for:
- Configuration Management: Separate config from code
- Security: Keep secrets out of source code
- Deployment: Different settings for different environments
- Flexibility: Change behavior without recompiling
Basic Environment Variable Operations
Reading Environment Variables
Go’s os package provides access to environment variables:
package main
import (
"fmt"
"os"
)
func main() {
// Get environment variable
port := os.Getenv("PORT")
if port == "" {
port = "8080" // default value
}
fmt.Printf("Server running on port %s\n", port)
// Get with default value helper
dbHost := getEnvOrDefault("DB_HOST", "localhost")
fmt.Printf("Database host: %s\n", dbHost)
}
// Helper function to get env var with default
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}Checking if Environment Variable Exists
package main
import (
"fmt"
"os"
)
func main() {
// Check if environment variable exists
value, exists := os.LookupEnv("API_KEY")
if exists {
fmt.Printf("API_KEY found: %s\n", value)
} else {
fmt.Println("API_KEY not set")
}
// Alternative way
if os.Getenv("DEBUG") != "" {
fmt.Println("Debug mode enabled")
}
}Setting Environment Variables
package main
import (
"fmt"
"os"
)
func main() {
// Set environment variable (only affects current process)
os.Setenv("APP_NAME", "MyGoApp")
// Verify it was set
appName := os.Getenv("APP_NAME")
fmt.Printf("App name: %s\n", appName)
// Clear environment variable
os.Unsetenv("APP_NAME")
// Verify it was cleared
if os.Getenv("APP_NAME") == "" {
fmt.Println("APP_NAME cleared")
}
}Working with Environment Files
Creating .env Files
Create a .env file for development:
# .env
PORT=8080
DB_HOST=localhost
DB_PORT=5432
DB_USER=myuser
DB_PASSWORD=mypassword
DEBUG=true
API_KEY=your-api-key-hereLoading .env Files
Go doesn’t have built-in .env file support, so you’ll need a third-party package:
go get github.com/joho/godotenvpackage main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
)
type Config struct {
Port string
DBHost string
DBPort string
DBUser string
DBPassword string
Debug bool
APIKey string
}
func main() {
// Load .env file
err := godotenv.Load()
if err != nil {
log.Println("Warning: Could not load .env file")
}
config := Config{
Port: getEnvOrDefault("PORT", "8080"),
DBHost: getEnvOrDefault("DB_HOST", "localhost"),
DBPort: getEnvOrDefault("DB_PORT", "5432"),
DBUser: getEnvOrDefault("DB_USER", ""),
DBPassword: getEnvOrDefault("DB_PASSWORD", ""),
Debug: getEnvBool("DEBUG"),
APIKey: getEnvOrDefault("API_KEY", ""),
}
fmt.Printf("Config: %+v\n", config)
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvBool(key string) bool {
return os.Getenv(key) == "true"
}Custom .env File Loader
If you prefer not to use external packages:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func loadEnvFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Split on first =
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Remove quotes if present
if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
value = strings.Trim(value, `"`)
}
os.Setenv(key, value)
}
}
return scanner.Err()
}
func main() {
// Load custom .env file
err := loadEnvFile(".env")
if err != nil {
fmt.Printf("Warning: Could not load .env file: %v\n", err)
}
port := os.Getenv("PORT")
fmt.Printf("Port: %s\n", port)
}Structured Configuration
Configuration Struct with Tags
package main
import (
"fmt"
"os"
"strconv"
"time"
)
type DatabaseConfig struct {
Host string `env:"DB_HOST" default:"localhost"`
Port int `env:"DB_PORT" default:"5432"`
User string `env:"DB_USER" required:"true"`
Password string `env:"DB_PASSWORD" required:"true"`
Name string `env:"DB_NAME" default:"myapp"`
Timeout time.Duration `env:"DB_TIMEOUT" default:"5s"`
}
type ServerConfig struct {
Port string `env:"PORT" default:"8080"`
Host string `env:"HOST" default:"0.0.0.0"`
ReadTimeout time.Duration `env:"READ_TIMEOUT" default:"30s"`
WriteTimeout time.Duration `env:"WRITE_TIMEOUT" default:"30s"`
Debug bool `env:"DEBUG" default:"false"`
}
type Config struct {
Server ServerConfig
Database DatabaseConfig
}
func main() {
config := &Config{
Server: ServerConfig{
Port: getEnvOrDefault("PORT", "8080"),
Host: getEnvOrDefault("HOST", "0.0.0.0"),
ReadTimeout: getEnvDuration("READ_TIMEOUT", 30*time.Second),
WriteTimeout: getEnvDuration("WRITE_TIMEOUT", 30*time.Second),
Debug: getEnvBool("DEBUG"),
},
Database: DatabaseConfig{
Host: getEnvOrDefault("DB_HOST", "localhost"),
Port: getEnvInt("DB_PORT", 5432),
User: getEnvRequired("DB_USER"),
Password: getEnvRequired("DB_PASSWORD"),
Name: getEnvOrDefault("DB_NAME", "myapp"),
Timeout: getEnvDuration("DB_TIMEOUT", 5*time.Second),
},
}
fmt.Printf("Server config: %+v\n", config.Server)
fmt.Printf("Database config: %+v\n", config.Database)
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvRequired(key string) string {
if value := os.Getenv(key); value != "" {
return value
}
panic(fmt.Sprintf("Required environment variable %s is not set", key))
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
func getEnvBool(key string) bool {
return os.Getenv(key) == "true"
}
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return defaultValue
}Environment-Specific Configuration
Multiple Environment Files
Create different .env files for each environment:
# .env.development
PORT=8080
DB_HOST=localhost
DEBUG=true
LOG_LEVEL=debug
# .env.staging
PORT=8080
DB_HOST=staging-db.example.com
DEBUG=false
LOG_LEVEL=info
# .env.production
PORT=80
DB_HOST=prod-db.example.com
DEBUG=false
LOG_LEVEL=errorpackage main
import (
"fmt"
"log"
"os"
"strings"
"github.com/joho/godotenv"
)
func loadEnvironment() {
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
// Load base .env file
if err := godotenv.Load(".env"); err == nil {
log.Printf("Loaded .env file")
}
// Load environment-specific file
envFile := fmt.Sprintf(".env.%s", env)
if err := godotenv.Load(envFile); err == nil {
log.Printf("Loaded %s file", envFile)
}
log.Printf("Running in %s environment", env)
}
func main() {
loadEnvironment()
config := map[string]string{
"Port": os.Getenv("PORT"),
"DBHost": os.Getenv("DB_HOST"),
"Debug": os.Getenv("DEBUG"),
"LogLevel": os.Getenv("LOG_LEVEL"),
}
fmt.Printf("Configuration: %+v\n", config)
}Best Practices
1. Use Descriptive Variable Names
// Good
const (
EnvDatabaseURL = "DATABASE_URL"
EnvAPISecret = "API_SECRET"
EnvServerPort = "SERVER_PORT"
)
// Bad
const (
EnvDB = "DB"
EnvSec = "SECRET"
EnvP = "PORT"
)2. Provide Sensible Defaults
type Config struct {
ServerPort string
DatabaseURL string
}
func NewConfig() *Config {
return &Config{
ServerPort: getEnvOrDefault("SERVER_PORT", "8080"),
DatabaseURL: getEnvRequired("DATABASE_URL"),
}
}3. Validate Required Variables
func validateConfig() error {
requiredVars := []string{
"DATABASE_URL",
"API_SECRET",
"REDIS_URL",
}
for _, varName := range requiredVars {
if os.Getenv(varName) == "" {
return fmt.Errorf("required environment variable %s is not set", varName)
}
}
return nil
}4. Use Environment-Specific Prefixes
const (
EnvPrefix = "MYAPP_"
EnvServerPort = EnvPrefix + "SERVER_PORT"
EnvDatabaseURL = EnvPrefix + "DATABASE_URL"
EnvLogLevel = EnvPrefix + "LOG_LEVEL"
)Security Considerations
1. Never Commit .env Files
Add to .gitignore:
# Environment files
.env
.env.local
.env.*.local
# OS generated files
.DS_Store
Thumbs.db2. Use Different Secrets for Different Environments
func getSecret() string {
env := os.Getenv("APP_ENV")
switch env {
case "production":
return os.Getenv("PRODUCTION_SECRET")
case "staging":
return os.Getenv("STAGING_SECRET")
default:
return os.Getenv("DEVELOPMENT_SECRET")
}
}3. Rotate Secrets Regularly
Implement secret rotation logic:
func getRotatingSecret() (string, error) {
primary := os.Getenv("API_SECRET_PRIMARY")
secondary := os.Getenv("API_SECRET_SECONDARY")
// Try primary secret first
if primary != "" {
return primary, nil
}
// Fall back to secondary during rotation
if secondary != "" {
return secondary, nil
}
return "", fmt.Errorf("no valid secret found")
}Testing with Environment Variables
Setting Test Environment
package main
import (
"os"
"testing"
)
func TestConfig(t *testing.T) {
// Set test environment variables
os.Setenv("PORT", "3000")
os.Setenv("DB_HOST", "localhost")
os.Setenv("DEBUG", "true")
// Clean up after test
defer func() {
os.Unsetenv("PORT")
os.Unsetenv("DB_HOST")
os.Unsetenv("DEBUG")
}()
config := NewConfig()
if config.ServerPort != "3000" {
t.Errorf("Expected port 3000, got %s", config.ServerPort)
}
}Using Test Tables
func TestGetEnvOrDefault(t *testing.T) {
tests := []struct {
name string
key string
value string
default string
expected string
}{
{"env var set", "TEST_KEY", "test_value", "default", "test_value"},
{"env var not set", "MISSING_KEY", "", "default", "default"},
{"empty env var", "EMPTY_KEY", "", "default", "default"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.value != "" {
os.Setenv(tt.key, tt.value)
defer os.Unsetenv(tt.key)
}
result := getEnvOrDefault(tt.key, tt.default)
if result != tt.expected {
t.Errorf("Expected %s, got %s", tt.expected, result)
}
})
}
}Docker and Environment Variables
Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
# Set default environment variables
ENV PORT=8080
ENV HOST=0.0.0.0
EXPOSE 8080
CMD ["./main"]Docker Compose
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
- DB_HOST=database
- DB_PORT=5432
- DB_USER=myuser
- DB_PASSWORD=mypassword
env_file:
- .env
depends_on:
- database
database:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypasswordResources
- Go os package documentation
- 12-Factor App - Config
- godotenv package
- Environment Variables Best Practices
Next Steps
Now that you understand environment variables in Go, learn how to build web applications with Gin or explore Go deployment strategies.
Last updated on