Skip to main content

env - Environment Variable Management Library

env is a simple and easy-to-use environment variable management library that provides .env file loading, type conversion, grouped queries, and other features. It adopts the "initialize once, read many times" design philosophy, suitable for high-concurrency scenarios.

Installation

go get -u go-slim.dev/env

Quick Start

Basic Usage

package main

import (
"log"
"go-slim.dev/env"
)

func main() {
// Initialize: load .env file
if err := env.Init(); err != nil {
log.Fatal(err)
}

// Lock environment variables to prevent runtime modifications
env.Lock()

// Read environment variables
port := env.Int("PORT", 8080)
debug := env.Bool("DEBUG", false)
dbHost := env.String("DB_HOST", "localhost")

log.Printf("Server running on port %d", port)
}

.env File Example

# Application configuration
APP_ENV=development
APP_NAME=MyApp
PORT=8080
DEBUG=true

# Database configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mydb
DB_USER=admin
DB_PASSWORD=secret

# Cache configuration
CACHE_DRIVER=redis
CACHE_DATABASE=1
CACHE_SCOPE=app:

Core Functions

1. Initialization Functions

Init - Initialize Environment Variables

func Init(root ...string) error

Description:

Loads the .env file from the runtime directory and initializes environment variables. This is the entry point for the entire library.

Parameters:

  • root: Optional parameter specifying the directory containing the .env file; if not specified, uses the current program's runtime directory

Loading Order:

  1. System environment variables
  2. .env file
  3. .env.local file (higher priority, can override .env)
  4. .env.{APP_ENV} file (based on APP_ENV variable)
  5. .env.{APP_ENV}.local file (highest priority)

Examples:

// Use current directory
if err := env.Init(); err != nil {
log.Fatal(err)
}

// Specify configuration directory
if err := env.Init("/path/to/config"); err != nil {
log.Fatal(err)
}

// Multi-environment configuration example
// .env.development, .env.production are automatically loaded based on APP_ENV

Notes:

  • This function is not thread-safe and must be called single-threaded during program startup
  • Later loaded files will override variables with the same name from earlier loaded files
  • Non-existent files will not cause errors; only read failures will return errors

InitWithDir - Initialize from Specified Directory

func InitWithDir(dir string) error

Description:

Loads the .env file from the specified directory. The difference from Init() is that a directory parameter must be provided.

Parameters:

  • dir: Absolute or relative path to the directory containing the .env file

Example:

// Load from specified directory
if err := env.InitWithDir("/etc/myapp"); err != nil {
log.Fatal(err)
}

Load - Load Specified File

func Load(filenames ...string) error

Description:

Loads specified environment variable files. Can be used to load additional configuration files.

Parameters:

  • filenames: List of file paths to load

Example:

// Load additional configurations after initialization
env.Init()
env.Load("/path/to/secrets.env")
env.Load("/path/to/custom1.env", "/path/to/custom2.env")

Notes:

  • Cannot be called after Lock()
  • Will return an error if files do not exist

Lock - Lock Environment Variables

func Lock()

Description:

Locks global environment variables to prevent subsequent modifications. Any write operations (Init, Load, Updates) will fail after calling.

Best Practice:

Call Lock() immediately after initialization to ensure only reading environment variables at runtime.

Example:

func main() {
// Initialization phase
if err := env.Init(); err != nil {
log.Fatal(err)
}

// Lock to prevent runtime modifications
env.Lock()

// Runtime phase - only reading allowed
go worker1()
go worker2()
}

Characteristics:

  • Locking is one-way and cannot be unlocked
  • Calling Load() after locking returns ErrLocked error
  • Calling Updates() or Clean() after locking causes panic
  • Can be called repeatedly (idempotent operation)

2. Query Functions

Lookup - Query Environment Variable

func Lookup(name string) (string, bool)

Description:

Queries the value of a specified environment variable.

Parameters:

  • name: Environment variable name

Return Values:

  • First return value: Variable's string value
  • Second return value: true if variable exists and value is non-empty, false if doesn't exist or is empty

Example:

if value, exists := env.Lookup("API_KEY"); exists {
fmt.Println("API Key:", value)
} else {
fmt.Println("API Key not set")
}

Exists - Check if Variable Exists

func Exists(name string) bool

Description:

Checks if an environment variable exists. Unlike Lookup, returns true as long as the variable is defined, even if the value is empty.

Example:

if env.Exists("DEBUG") {
// DEBUG variable is defined (may be empty)
}

// Compare with Lookup
value, exists := env.Lookup("DEBUG")
// exists is only true when DEBUG exists and is non-empty

String - Get String Value

func String(name string, fallback ...string) string

Description:

Returns the string value of the specified environment variable. If it doesn't exist or is empty, returns the default value.

Parameters:

  • name: Environment variable name
  • fallback: Optional default value (variadic parameter, only uses the first one)

Example:

// No default value, returns empty string if doesn't exist
host := env.String("DB_HOST")

// With default value
host := env.String("DB_HOST", "localhost")
name := env.String("APP_NAME", "MyApp")

Int - Get Integer Value

func Int(name string, fallback ...int) int

Description:

Returns the integer value of the specified environment variable. If it doesn't exist, is empty, or conversion fails, returns the default value.

Example:

// No default value, returns 0 on failure
port := env.Int("PORT")

// With default value
port := env.Int("PORT", 8080)
maxConn := env.Int("MAX_CONNECTIONS", 100)

Float - Get Float Value

func Float(name string, fallback ...float64) float64

Description:

Returns the float value of the specified environment variable. If it doesn't exist, is empty, or conversion fails, returns the default value.

Example:

rate := env.Float("TAX_RATE", 0.08)
ratio := env.Float("COMPRESSION_RATIO", 1.0)

Bool - Get Boolean Value

func Bool(name string, fallback ...bool) bool

Description:

Returns the boolean value of the specified environment variable. Supported values:

  • true: "1", "t", "T", "true", "TRUE", "True"
  • false: "0", "f", "F", "false", "FALSE", "False"

Example:

debug := env.Bool("DEBUG", false)
verbose := env.Bool("VERBOSE", true)

Duration - Get Time Duration Value

func Duration(name string, fallback ...time.Duration) time.Duration

Description:

Returns the time duration value of the specified environment variable. Supports Go standard time format (like "1h30m") or integers (treated as nanoseconds).

Example:

// .env file
// TIMEOUT=30s
// INTERVAL=5m

timeout := env.Duration("TIMEOUT", 10*time.Second)
interval := env.Duration("INTERVAL", 1*time.Minute)

Bytes - Get Byte Slice Value

func Bytes(name string, fallback ...[]byte) []byte

Description:

Returns the byte slice value of the specified environment variable.

Example:

data := env.Bytes("BINARY_DATA")
key := env.Bytes("SECRET_KEY", []byte("default-key"))

List - Get String List

func List(name string, fallback ...[]string) []string

Description:

Returns a string list from the specified environment variable. Values are split by commas, and each element is automatically trimmed.

Example:

// .env file
// ALLOWED_ORIGINS=http://localhost:3000,https://example.com,https://api.example.com
// TAGS=go, web, api

origins := env.List("ALLOWED_ORIGINS")
// []string{"http://localhost:3000", "https://example.com", "https://api.example.com"}

tags := env.List("TAGS")
// []string{"go", "web", "api"} (automatically trimmed)

// Using default value
hosts := env.List("REDIS_HOSTS", []string{"localhost:6379"})

3. Batch Query Functions

Map - Get All Variables with Same Prefix

func Map(prefix string) map[string]string

Description:

Aggregates environment variables with the same prefix into a map. The returned keys have the prefix removed.

Example:

// .env file
// DB_HOST=localhost
// DB_PORT=5432
// DB_NAME=mydb
// DB_USER=admin

dbConfig := env.Map("DB_")
// map[string]string{
// "HOST": "localhost",
// "PORT": "5432",
// "NAME": "mydb",
// "USER": "admin",
// }

Where - Custom Filter Query

func Where(filter func(name, value string) bool) map[string]string

Description:

Returns environment variables filtered by a custom filter function.

Example:

// Get all variables starting with API_
apiVars := env.Where(func(name, value string) bool {
return strings.HasPrefix(name, "API_")
})

// Get all variables with value "true"
trueVars := env.Where(func(name, value string) bool {
return value == "true"
})

// Get all non-empty variables
nonEmpty := env.Where(func(name, value string) bool {
return value != ""
})

All - Get All Environment Variables

func All() map[string]string

Description:

Returns all environment variables (including system environment variables and variables from .env files).

Example:

allVars := env.All()
for name, value := range allVars {
fmt.Printf("%s=%s\n", name, value)
}

4. Structure Filling

Fill - Fill Structure

func Fill(structure any) error

Description:

Automatically fills structure fields using environment variables. Specify the corresponding environment variable name via the env tag.

Example:

// .env file
// APP_NAME=MyApp
// APP_PORT=8080
// APP_DEBUG=true
// DB_HOST=localhost
// DB_PORT=5432

type Config struct {
AppName string `env:"APP_NAME"`
Port int `env:"APP_PORT"`
Debug bool `env:"APP_DEBUG"`
Database struct {
Host string `env:"DB_HOST"`
Port int `env:"DB_PORT"`
}
}

var cfg Config
if err := env.Fill(&cfg); err != nil {
log.Fatal(err)
}

fmt.Println(cfg.AppName) // MyApp
fmt.Println(cfg.Port) // 8080
fmt.Println(cfg.Debug) // true

Supported Types:

  • Basic types: string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool
  • Time types: time.Time, time.Duration
  • Slice types: []int, []string, etc.
  • Nested structures

Notes:

  • Must pass a structure pointer
  • Nested structures are recursively filled
  • Fields must be exported (start with uppercase letter)
  • Unsupported types will return an error

5. Grouped Query - Signed

Signed - Create Grouped Querier

func Signed(prefix, category string) Signer

Description:

Creates a grouped querier for operating on environment variables with the same prefix but needing to distinguish different scenarios.

Query Strategy:

Uses two-level lookup with fallback mechanism:

  1. First lookup: PREFIX_CATEGORY_KEY
  2. If not found, fallback to: PREFIX_KEY

This allows defining common configurations at the prefix level and overriding specific configurations at the category level.

Example:

// .env file
// CACHE_DRIVER=redis
// CACHE_DATABASE=1
// CACHE_SCOPE=app:
// CACHE_BOOK_DATABASE=10
// CACHE_BOOK_SCOPE=app:books:
// CACHE_USER_DATABASE=11
// CACHE_USER_SCOPE=app:users:

// Get BOOK category cache configuration
bookCache := env.Signed("CACHE", "BOOK")
bookCache.String("DRIVER") // "redis" (fallback to CACHE_DRIVER)
bookCache.Int("DATABASE") // 10 (from CACHE_BOOK_DATABASE)
bookCache.String("SCOPE") // "app:books:" (from CACHE_BOOK_SCOPE)

// Get USER category cache configuration
userCache := env.Signed("CACHE", "USER")
userCache.String("DRIVER") // "redis" (fallback to CACHE_DRIVER)
userCache.Int("DATABASE") // 11 (from CACHE_USER_DATABASE)
userCache.String("SCOPE") // "app:users:" (from CACHE_USER_SCOPE)

// Get default cache configuration (no category specified)
defaultCache := env.Signed("CACHE", "")
defaultCache.String("DRIVER") // "redis"
defaultCache.Int("DATABASE") // 1
defaultCache.String("SCOPE") // "app:"

Signer Interface Methods:

The Signer interface provides the same query methods as global functions:

  • Lookup(key string) (string, bool)
  • Exists(key string) bool
  • String(key string, fallback ...string) string
  • Bytes(key string, fallback ...[]byte) []byte
  • Int(key string, fallback ...int) int
  • Duration(key string, fallback ...time.Duration) time.Duration
  • Bool(key string, fallback ...bool) bool
  • List(key string, fallback ...[]string) []string
  • Map(prefix string) map[string]string
  • Where(filter func(name, value string) bool) map[string]string
  • Fill(structure any) error

Use Cases:

Suitable for configuring similar but different environment variables for different modules, environments, or tenants:

// Multi-tenant configuration
tenant1DB := env.Signed("DB", "TENANT1")
tenant2DB := env.Signed("DB", "TENANT2")

// Multi-service configuration
authService := env.Signed("SERVICE", "AUTH")
apiService := env.Signed("SERVICE", "API")

// Multi-environment cache
prodCache := env.Signed("CACHE", "PROD")
devCache := env.Signed("CACHE", "DEV")

6. Helper Functions

Path - Get Path Relative to Initialization Directory

func Path(path ...string) string

Description:

Returns an absolute path based on the initialization directory.

Example:

// Assume initialization directory is /app
env.Init("/app")

env.Path() // "/app"
env.Path("config") // "/app/config"
env.Path("logs", "app.log") // "/app/logs/app.log"

Is - Check Application Environment

func Is(env ...string) bool

Description:

Checks if the APP_ENV environment variable matches any of the given values.

Example:

// .env file
// APP_ENV=development

if env.Is("development", "dev") {
// Development environment
log.Println("Running in development mode")
}

if env.Is("production", "prod") {
// Production environment
} else {
// Non-production environment
}

Updates - Update Environment Variables

func Updates(data map[string]string) bool

Description:

Batch updates environment variables. Returns true for success, false for failure (e.g., already locked).

Example:

// Dynamically set environment variables
success := env.Updates(map[string]string{
"FEATURE_FLAG_A": "true",
"FEATURE_FLAG_B": "false",
})

if !success {
log.Println("Failed to update environment")
}

Notes:

  • Cannot be used after calling Lock()
  • Not thread-safe, only use during initialization phase

Use Cases

1. Multi-Environment Configuration

// Project structure
// .env # Default configuration
// .env.local # Local override (not committed to git)
// .env.development # Development environment
// .env.development.local
// .env.production # Production environment
// .env.production.local

// main.go
func main() {
// Automatically loads corresponding configuration based on APP_ENV
if err := env.Init(); err != nil {
log.Fatal(err)
}
env.Lock()

// Execute different logic based on environment
if env.Is("development") {
// Development environment configuration
gin.SetMode(gin.DebugMode)
} else if env.Is("production") {
// Production environment configuration
gin.SetMode(gin.ReleaseMode)
}
}

2. Database Configuration Management

type DatabaseConfig struct {
Host string `env:"DB_HOST"`
Port int `env:"DB_PORT"`
Database string `env:"DB_NAME"`
Username string `env:"DB_USER"`
Password string `env:"DB_PASSWORD"`
MaxConns int `env:"DB_MAX_CONNS"`
SSLMode string `env:"DB_SSL_MODE"`
}

func NewDatabaseConfig() (*DatabaseConfig, error) {
cfg := &DatabaseConfig{}
if err := env.Fill(cfg); err != nil {
return nil, err
}
return cfg, nil
}

func main() {
env.Init()
env.Lock()

dbCfg, err := NewDatabaseConfig()
if err != nil {
log.Fatal(err)
}

dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
dbCfg.Host, dbCfg.Port, dbCfg.Database,
dbCfg.Username, dbCfg.Password, dbCfg.SSLMode)
}

3. Multi-Tenant/Multi-Instance Configuration

// .env file
// REDIS_HOST=localhost
// REDIS_PORT=6379
// REDIS_SESSION_DATABASE=0
// REDIS_SESSION_PREFIX=session:
// REDIS_CACHE_DATABASE=1
// REDIS_CACHE_PREFIX=cache:
// REDIS_QUEUE_DATABASE=2
// REDIS_QUEUE_PREFIX=queue:

// Create different Redis clients for different purposes
func setupRedis() {
env.Init()
env.Lock()

// Session Redis
sessionRedis := env.Signed("REDIS", "SESSION")
sessionClient := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
sessionRedis.String("HOST", "localhost"),
sessionRedis.Int("PORT", 6379)),
DB: sessionRedis.Int("DATABASE", 0),
})

// Cache Redis
cacheRedis := env.Signed("REDIS", "CACHE")
cacheClient := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
cacheRedis.String("HOST", "localhost"),
cacheRedis.Int("PORT", 6379)),
DB: cacheRedis.Int("DATABASE", 1),
})

// Queue Redis
queueRedis := env.Signed("REDIS", "QUEUE")
queueClient := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
queueRedis.String("HOST", "localhost"),
queueRedis.Int("PORT", 6379)),
DB: queueRedis.Int("DATABASE", 2),
})
}

4. Feature Flags

// .env file
// FEATURE_NEW_UI=true
// FEATURE_BETA_API=false
// FEATURE_ANALYTICS=true

type Features struct {
NewUI bool `env:"FEATURE_NEW_UI"`
BetaAPI bool `env:"FEATURE_BETA_API"`
Analytics bool `env:"FEATURE_ANALYTICS"`
}

func main() {
env.Init()
env.Lock()

features := &Features{}
env.Fill(features)

if features.NewUI {
// Enable new UI
}

if features.BetaAPI {
// Enable Beta API
}
}

5. Service Discovery Configuration

// .env file
// SERVICES=auth,api,notification,payment
// SERVICE_AUTH_URL=http://auth:8001
// SERVICE_API_URL=http://api:8002
// SERVICE_NOTIFICATION_URL=http://notification:8003
// SERVICE_PAYMENT_URL=http://payment:8004

func loadServiceURLs() map[string]string {
env.Init()
env.Lock()

services := env.List("SERVICES")
urls := make(map[string]string)

for _, service := range services {
signer := env.Signed("SERVICE", strings.ToUpper(service))
if url, exists := signer.Lookup("URL"); exists {
urls[service] = url
}
}

return urls
}

// Usage
serviceURLs := loadServiceURLs()
authURL := serviceURLs["auth"] // http://auth:8001
apiURL := serviceURLs["api"] // http://api:8002

Thread Safety Notes

Design Model: "Initialize Once, Read Many Times"

The env library adopts a clear two-phase design:

Initialization Phase (single-threaded):

  • Call Init(), InitWithDir(), Load(), Updates()
  • Must complete before starting any goroutines
  • These functions are not thread-safe

Runtime Phase (multi-threaded):

  • Call Lookup(), String(), Int(), etc., read functions
  • All read operations are thread-safe
  • Multiple goroutines can concurrently read

Correct Usage

func main() {
// ✓ Correct: Single-threaded initialization at startup
if err := env.Init(); err != nil {
log.Fatal(err)
}

// ✓ Best practice: Lock to prevent accidental modifications
env.Lock()

// ✓ Correct: Read operations are thread-safe
go func() {
cache := env.Signed("CACHE", "BOOK")
driver := cache.String("DRIVER") // Safe
}()

go func() {
port := env.Int("PORT", 8080) // Safe
}()
}

Incorrect Usage

func main() {
// ✗ Wrong: Concurrent calls to Init will cause data races
go env.Init() // Don't do this!
go env.Init() // Don't do this!

// ✗ Wrong: Modifying environment variables at runtime will cause data races
go func() {
env.Load(".env.runtime") // Don't do this at runtime!
}()
}

Why No Locks Are Needed?

  • Initialization Phase: Single-threaded execution, no need for locks
  • Runtime Phase: Read-only access, Go's slice reading is inherently thread-safe
  • Environment Data: Never changes after initialization completes

The Lock() function's purpose is to enforce the read-only convention at runtime, preventing accidental write operations.


API Reference

Global Functions

FunctionDescriptionThread-Safe
Init(root ...string) errorInitialize env varsNo
InitWithDir(dir string) errorInitialize from dirNo
Load(filenames ...string) errorLoad specified filesNo
Lock()Lock env varsYes
Signed(prefix, category string) SignerCreate grouped querierYes
Path(path ...string) stringGet relative pathYes
Is(env ...string) boolCheck app environmentYes
Updates(data map[string]string) boolUpdate env varsNo

Query Functions (Thread-Safe)

FunctionDescription
Lookup(name string) (string, bool)Query env var
Exists(name string) boolCheck existence
String(name string, fallback ...string) stringGet string value
Bytes(name string, fallback ...[]byte) []byteGet byte slice
Int(name string, fallback ...int) intGet integer value
Float(name string, fallback ...float64) float64Get float value
Bool(name string, fallback ...bool) boolGet boolean value
Duration(name string, fallback ...time.Duration) time.DurationGet time duration
List(name string, fallback ...[]string) []stringGet string list
Map(prefix string) map[string]stringGet vars with same prefix
Where(filter func(name, value string) bool) map[string]stringCustom filter query
Fill(structure any) errorFill structure
All() map[string]stringGet all env vars

Signer Interface

Signer provides the same query methods as global functions but automatically adds prefix and category.


Notes

1. Initialization Order

The loading order of environment variables affects the final values (later loaded overrides earlier loaded):

System environment variables

.env
(override)
.env.local
(override)
.env.{APP_ENV}
(override)
.env.{APP_ENV}.local

2. .gitignore Configuration

Recommended .gitignore configuration:

# Don't commit local configurations
.env.local
.env.*.local

# Optional: also don't commit .env
.env

Commit .env as a template, use .env.local to store local sensitive configurations.

3. Environment Variable Naming Convention

Recommended naming convention:

  • Use uppercase letters and underscores
  • Use prefixes to group related configurations (like DB_, CACHE_, REDIS_)
  • Use middle part for categories (like CACHE_BOOK_, CACHE_USER_)
  • Use suffixes for specific config items (like _HOST, _PORT, _DATABASE)

Example:

# Good naming
DB_HOST=localhost
DB_PORT=5432
CACHE_REDIS_HOST=localhost
CACHE_REDIS_PORT=6379

# Not recommended
dbhost=localhost # Lowercase
database-port=5432 # Uses hyphens
redis.host=localhost # Uses dots

4. Type Conversion Failures

When type conversion fails, the default value is returned:

// .env file
// PORT=invalid

port := env.Int("PORT", 8080) // Returns 8080 (conversion failed, uses default)

If you need to know if conversion succeeded, use Lookup with manual conversion:

if value, exists := env.Lookup("PORT"); exists {
if port, err := strconv.Atoi(value); err == nil {
// Conversion succeeded
} else {
// Conversion failed
log.Printf("Invalid PORT value: %s", value)
}
}

5. Sensitive Information Protection

Don't commit sensitive information (passwords, keys, etc.) to version control:

// Use .env.local to store sensitive information
// .env.local (not committed to git)
DB_PASSWORD=super_secret_password
API_SECRET_KEY=your_secret_key_here

// .env (can be committed, provides template)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mydb
DB_USER=admin
# DB_PASSWORD= # Set in .env.local

6. Using Default Values

Reasonable use of default values can simplify configuration:

// Reasonable defaults for development environment
port := env.Int("PORT", 8080)
debug := env.Bool("DEBUG", true) // Default enabled in development
timeout := env.Duration("TIMEOUT", 30*time.Second)

// This way .env file only needs to configure values that need to change

7. Structure Fill Limitations

Limitations of the Fill() function:

// ✓ Supported
type Config struct {
Port int `env:"PORT"` // Exported field + env tag
}

// ✗ Not supported
type Config struct {
port int `env:"PORT"` // Unexported field
Port int // Missing env tag
}

8. Signed Lookup Order

Understanding the Signed fallback mechanism:

// Environment variables:
// CACHE_DRIVER=redis
// CACHE_BOOK_DATABASE=10

cache := env.Signed("CACHE", "BOOK")

// Looking up DRIVER
// 1. Try CACHE_BOOK_DRIVER (doesn't exist)
// 2. Fallback to CACHE_DRIVER (found: "redis")

// Looking up DATABASE
// 1. Try CACHE_BOOK_DATABASE (found: 10)
// 2. No need to fallback

Best Practices

1. Standard Initialization Flow

func main() {
// 1. Initialize environment variables
if err := env.Init(); err != nil {
log.Fatalf("Failed to initialize env: %v", err)
}

// 2. Lock to prevent runtime modifications
env.Lock()

// 3. Load configuration
cfg := loadConfig()

// 4. Start application
startApp(cfg)
}

func loadConfig() *Config {
cfg := &Config{}
if err := env.Fill(cfg); err != nil {
log.Fatalf("Failed to load config: %v", err)
}
return cfg
}

2. Structured Configuration

// Organize related configurations into structures
type AppConfig struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
Logger LoggerConfig
}

type ServerConfig struct {
Port int `env:"SERVER_PORT"`
Host string `env:"SERVER_HOST"`
Timeout time.Duration `env:"SERVER_TIMEOUT"`
}

type DatabaseConfig struct {
Host string `env:"DB_HOST"`
Port int `env:"DB_PORT"`
Database string `env:"DB_NAME"`
Username string `env:"DB_USER"`
Password string `env:"DB_PASSWORD"`
}

func LoadAppConfig() (*AppConfig, error) {
cfg := &AppConfig{}
if err := env.Fill(cfg); err != nil {
return nil, err
}
return cfg, nil
}

3. Using Signed to Manage Multiple Instances

// Use different configurations for different service instances
func setupServices() {
authService := setupService("AUTH")
apiService := setupService("API")
notifyService := setupService("NOTIFICATION")
}

func setupService(name string) *Service {
cfg := env.Signed("SERVICE", name)
return &Service{
URL: cfg.String("URL"),
Timeout: cfg.Duration("TIMEOUT", 30*time.Second),
APIKey: cfg.String("API_KEY"),
}
}

4. Provide Configuration Template

Provide a .env.example file as a configuration template in the project:

# .env.example - Configuration template (commit to git)

# Application configuration
APP_ENV=development
APP_NAME=MyApp
APP_PORT=8080
APP_DEBUG=true

# Database configuration (fill in actual values)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=
DB_USER=
DB_PASSWORD=

# Redis configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DATABASE=0

Users copy and rename to .env, then fill in actual values.