The Go Template uses environment variables for all runtime configuration, with support for .env files during development. Configuration is loaded and validated at startup, failing fast if required values are missing.
Environment Variables
All configuration is defined in the Config struct with struct tags:
type Config struct {
// [postgres]
DatabaseURL string `env:"DATABASE_URL,required"`
// [/postgres]
}
Available Variables
PostgreSQL connection string in the format: postgres://user:password@host:port/database?sslmode=disable
Example: postgres://postgres:postgres@localhost:5432/app?sslmode=disable
Use sslmode=require in production to enforce encrypted connections.
Controls logging verbosity. Valid values:
debug - Verbose output for development
info - Standard operational logs
warn - Warning messages only
error - Error messages only
Example:
.env File
During development, environment variables are loaded from a .env file in the project root:
# Log level: debug, info, warn, error
LOG_LEVEL = debug
# [postgres]
# Postgres Database URL
# Point this at any postgres instance (local, docker, external).
# To start the bundled postgres: make db
DATABASE_URL = postgres://postgres:postgres@localhost:5432/app? sslmode = disable
# [/postgres]
Edit Values
Modify .env with your local settings. This file is gitignored.
Run Application
The .env file is automatically loaded:
How .env Loading Works
The pkg/env package implements a simple .env parser:
func Load ( path ... string ) error {
filename := ".env"
if len ( path ) > 0 && path [ 0 ] != "" {
filename = path [ 0 ]
}
// Repeated calls are no-ops (cached)
mu . Lock ()
defer mu . Unlock ()
if err , ok := loaded [ filename ]; ok {
return err
}
err := load ( filename )
loaded [ filename ] = err
return err
}
Key behaviors:
Process environment takes precedence : Existing env vars are never overwritten
Missing file is OK : Returns nil if .env doesn’t exist
Quoted values : Strips surrounding quotes from values
Comments : Lines starting with # are ignored
Thread-safe : Uses mutex for concurrent-safe loading
Never commit .env to version control. It may contain secrets like database passwords or API keys.
Adding New Configuration
To add a new configuration option:
Update Config Struct
Add a field with an env tag to pkg/env/config.go: type Config struct {
DatabaseURL string `env:"DATABASE_URL,required"`
LogLevel string `env:"LOG_LEVEL"`
// New configuration
APIKey string `env:"API_KEY,required"`
Timeout string `env:"REQUEST_TIMEOUT"`
}
Use ,required to make the variable mandatory.
Update .env.example
Document the new variable: # API key for external service
API_KEY = your-api-key-here
# HTTP request timeout (default: 30s)
REQUEST_TIMEOUT = 30s
Update Documentation
Add the variable to this page and any relevant guides.
Use in Application
Access via the config object: config , err := env . New ()
if err != nil {
return err
}
client := api . New ( config . APIKey )
Validation Example
The populate function validates required fields at startup:
func populate ( config * Config ) error {
v := reflect . ValueOf ( config ). Elem ()
t := v . Type ()
var missing [] string
for i := range t . NumField () {
field := t . Field ( i )
tag := field . Tag . Get ( "env" )
if tag == "" {
continue
}
name , opts , _ := strings . Cut ( tag , "," )
required := opts == "required"
val := os . Getenv ( name )
if val == "" && required {
missing = append ( missing , name )
continue
}
if field . Type . Kind () == reflect . String {
v . Field ( i ). SetString ( val )
}
}
if len ( missing ) > 0 {
return fmt . Errorf ( "env: missing required variables: %s " ,
strings . Join ( missing , ", " ))
}
return nil
}
This ensures the application fails immediately if required configuration is missing, rather than failing later at runtime.
Production Configuration
Docker
Pass environment variables to containers:
services :
app :
image : myapp:latest
environment :
- DATABASE_URL=postgres://user:pass@db:5432/app
- LOG_LEVEL=info
Or use an env file:
services :
app :
image : myapp:latest
env_file :
- production.env
Kubernetes
Use ConfigMaps for non-sensitive data:
apiVersion : v1
kind : ConfigMap
metadata :
name : app-config
data :
LOG_LEVEL : "info"
---
apiVersion : apps/v1
kind : Deployment
spec :
template :
spec :
containers :
- name : app
envFrom :
- configMapRef :
name : app-config
Use Secrets for sensitive data:
apiVersion : v1
kind : Secret
metadata :
name : app-secrets
type : Opaque
stringData :
DATABASE_URL : "postgres://..."
---
apiVersion : apps/v1
kind : Deployment
spec :
template :
spec :
containers :
- name : app
envFrom :
- secretRef :
name : app-secrets
AWS ECS
Define environment variables in task definitions:
{
"containerDefinitions" : [
{
"name" : "app" ,
"image" : "myapp:latest" ,
"environment" : [
{ "name" : "LOG_LEVEL" , "value" : "info" }
],
"secrets" : [
{
"name" : "DATABASE_URL" ,
"valueFrom" : "arn:aws:secretsmanager:region:account:secret:db-url"
}
]
}
]
}
Configuration Best Practices
Only optional configuration should have defaults. Required values (like DATABASE_URL) should fail fast if missing. // Good: Required value, no default
DatabaseURL string `env:"DATABASE_URL,required"`
// Good: Optional value with application default
LogLevel string `env:"LOG_LEVEL"` // Defaults to "info" in log package
// Bad: Required value with misleading default
APIKey string `env:"API_KEY"` // Don't default to empty string
Never hardcode credentials or API keys: // Bad: Hardcoded secret
const apiKey = "sk-1234567890"
// Good: From environment
apiKey := config . APIKey
Use secret management in production:
AWS Secrets Manager
HashiCorp Vault
Kubernetes Secrets
Validate configuration at startup, not when first used: func New () ( * Config , error ) {
if err := Load (); err != nil {
return nil , fmt . Errorf ( "env: load: %w " , err )
}
var config Config
if err := populate ( & config ); err != nil {
return nil , err // Fail fast if required vars missing
}
return & config , nil
}
Every environment variable should be documented: # API timeout in seconds (default: 30)
API_TIMEOUT = 30
# Enable debug mode (true/false)
DEBUG = false
# Comma-separated list of allowed origins
ALLOWED_ORIGINS = http://localhost:3000,https://example.com
Environment-Specific Files
Use different env files per environment: .env.development
.env.staging
.env.production
Load the appropriate one: env := os . Getenv ( "APP_ENV" )
if env == "" {
env = "development"
}
envFile := fmt . Sprintf ( ".env. %s " , env )
if err := env . Load ( envFile ); err != nil {
return err
}
Type-Safe Configuration
The current implementation only supports string fields. To add type safety:
type Config struct {
DatabaseURL string `env:"DATABASE_URL,required"`
RequestTimeout time . Duration `env:"REQUEST_TIMEOUT"`
MaxRetries int `env:"MAX_RETRIES"`
EnableDebug bool `env:"DEBUG"`
}
Extend the populate function to handle additional types:
switch field . Type . Kind () {
case reflect . String :
v . Field ( i ). SetString ( val )
case reflect . Int :
n , _ := strconv . Atoi ( val )
v . Field ( i ). SetInt ( int64 ( n ))
case reflect . Bool :
b , _ := strconv . ParseBool ( val )
v . Field ( i ). SetBool ( b )
}
Or use a library like kelseyhightower/envconfig for advanced parsing.
Troubleshooting
Config Load Errors
Missing required variable
Error: env: missing required variables: DATABASE_URL
Solution: Add the missing variable to your .env file or environment.
This is expected if running without a .env file. Set environment variables directly: export DATABASE_URL = postgres ://...
make dev
Check that process environment variables aren’t overriding your .env file: # Unset conflicting variables
unset DATABASE_URL
# Or explicitly use .env values
env -i $( cat .env | xargs ) make dev
Next Steps
Architecture Understand how configuration fits into the application architecture
Project Structure Learn where configuration files live in the project