Skip to content

Quick Start ​

Get started with the env library in 5 minutes, from installation to practical usage.

Installation ​

bash
go get github.com/cybergodev/env

Requirements

Go 1.25+

Create a .env File ​

Create a .env file in your project root:

bash
# Database configuration
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=secret

# Application configuration
DEBUG=true
APP_NAME=myapp
LOG_LEVEL=info

# Multiple values (comma-separated)
ALLOWED_HOSTS=localhost,example.com,api.example.com

Full syntax reference

For the complete .env syntax rules (quoting, escapes, multi-line values), see File Format.

Minimal Usage ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	// Load the .env file and apply to the system environment
	if err := env.Load(".env"); err != nil {
		panic(err)
	}

	// Get environment variables
	host := env.GetString("DB_HOST", "localhost")
	port := env.GetInt("DB_PORT", 5432)

	fmt.Printf("Server: %s:%d\n", host, port)
	// Output: Server: localhost:5432
}

Two usage modes

env provides two usage modes:

ModeUsageUse Case
Global modeenv.Load() + env.GetString()Simple applications, scripts, quick prototyping
Instance modeenv.New() + loader.GetString()Multiple instances, test isolation, fine-grained lifecycle control

The global mode uses package-level functions backed by a default Loader singleton. After calling env.Load(), all env.GetXxx() calls use that instance. The instance mode creates an independent Loader via env.New(), suitable for scenarios requiring isolation or managing multiple configurations simultaneously.

The examples in this documentation use the global mode by default. See the Multi-environment Configuration section for the full instance mode usage.

Note that forgetting to call Load() does not raise an error: before initialization, Get*/Lookup silently return default or zero values, while Set/Delete/Validate/ParseInto return ErrNotInitialized. See Package Functions for details.

Reading Values - All Types ​

Basic Types ​

go
// === With default values ===

// String - returns default "localhost" when not found
host := env.GetString("HOST", "localhost")

// Integer (int64) - returns default 8080 when not found
port := env.GetInt("PORT", 8080)

// Float (float64) - returns default 0.99 when not found
rate := env.GetFloat64("RATE", 0.99)

// Unsigned integer (uint64) - returns default 4096 when not found
size := env.GetUint64("MAX_SIZE", 4096)

// Boolean - returns default false when not found
debug := env.GetBool("DEBUG", false)

// Duration - returns default 30s when not found
timeout := env.GetDuration("TIMEOUT", 30*time.Second)
go
// === Without default values ===

// String - returns empty string "" when not found
host := env.GetString("HOST")

// Integer (int64) - returns 0 when not found
port := env.GetInt("PORT")

// Boolean - returns false when not found
debug := env.GetBool("DEBUG")

// Duration - returns 0 when not found
timeout := env.GetDuration("TIMEOUT")

Key-name resolution

The library supports multiple key access methods:

go
// JSON: {"app": {"name": "myapp"}}
// Stored as: APP_NAME=myapp

// All of the following can access this value
name := env.GetString("APP_NAME")      // Flat key (recommended)
name = env.GetString("app.name")       // Dot path (auto-converted)
name = env.GetString("APP.NAME")       // Uppercase dot path

Resolution rules:

  1. Exact match: Looks up the exact key KEY first
  2. Uppercase conversion: Lowercase keys try the uppercase version key → KEY
  3. Path resolution: Dot paths are converted to underscores app.name → APP_NAME

Boolean Support ​

GetBool supports the following values (case-insensitive):

TruthyFalsy
true, 1, yes, on, enabledfalse, 0, no, off, disabled

Slice Types ​

go
// String slice
hosts := env.GetSlice[string]("HOSTS", []string{"localhost"})

// Integer slices (supports int, int64, uint, uint64)
ports := env.GetSlice[int64]("PORTS", []int64{80, 443})
portsInt := env.GetSlice[int]("PORTS")  // also supports int

// Float slice
rates := env.GetSlice[float64]("RATES", []float64{0.1, 0.2})

// Boolean slice
flags := env.GetSlice[bool]("FLAGS", []bool{true, false})

// Duration slice
timeouts := env.GetSlice[time.Duration]("TIMEOUTS")

Parse order:

  1. First looks up indexed keys KEY_0, KEY_1, KEY_2...
  2. If no indexed keys, parses the KEY value by comma separation
go
// Method 1: Indexed keys (recommended)
// HOSTS_0=localhost
// HOSTS_1=example.com
hosts := env.GetSlice[string]("HOSTS")  // ["localhost", "example.com"]

// Method 2: Comma-separated
// PORTS=80,443,8080
ports := env.GetSlice[int64]("PORTS")  // [80, 443, 8080]

Lookup and Inspection ​

go
// Check if a key exists
value, exists := env.Lookup("API_KEY")
if !exists {
    // key does not exist
}

// Get all keys
keys := env.Keys()

// Get all key-value pairs
all := env.All()

// Get variable count
count := env.Len()

Secure Values ​

go
secret := env.GetSecure("API_KEY")
if secret != nil {
    defer secret.Release()

    // Get the raw value (only call when plaintext is needed, e.g. for crypto, API calls)
    value := secret.Reveal()

    // Use mask for logging (prevent leakage)
    log.Printf("API Key: %s", secret.Masked())  // Output: [SECURE:32 bytes]
}

Struct Mapping ​

Use struct tags to map environment variables to a struct:

go
package main

import (
	"fmt"
	"time"

	"github.com/cybergodev/env"
)

type Config struct {
	Host     string        `env:"DB_HOST" envDefault:"localhost"`
	Port     int64         `env:"DB_PORT" envDefault:"5432"`
	Password string        `env:"DB_PASSWORD"`
	Debug    bool          `env:"DEBUG" envDefault:"false"`
	Timeout  time.Duration `env:"TIMEOUT" envDefault:"30s"`
	Hosts    []string      `env:"ALLOWED_HOSTS"`
}

func main() {
	env.Load(".env")

	var cfg Config
	if err := env.ParseInto(&cfg); err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", cfg)
	// Output: {Host:localhost Port:5432 Password:secret Debug:true Timeout:30s Hosts:[localhost example.com api.example.com]}
}
Details

Struct Mapping guide.

Configuration Presets ​

The library provides four preset configurations for different scenarios:

PresetPurposeCharacteristics
DefaultConfig()General useSafe defaults, suitable for most cases
DevelopmentConfig()DevelopmentRelaxed limits, allows overwriting
TestingConfig()TestingCompact limits, allows overwriting, suited for unit tests
ProductionConfig()ProductionStrict validation + audit logging
go
// Development - relaxed limits
cfg := env.DevelopmentConfig()

// Testing - compact limits
cfg := env.TestingConfig()

// Production - strict validation + audit logging
cfg := env.ProductionConfig()

Detailed Preset Comparison ​

FeatureDefaultDevelopmentTestingProduction
Overwrite existing variables✗✓✓✗
Error on missing file✗✗✗✓
Audit logging✗✗✗✓
YAML syntax✗✓✗✗
File size limit2MB10MB64KB64KB
Max variables5005005050
Forbidden key check✓✓✓✓
Value validation✓✓✓✓

How to choose

  • Development: Use DevelopmentConfig() with relaxed limits for fast iteration
  • Testing: Use TestingConfig() with overwrite support for test isolation
  • Production: Use ProductionConfig() with audit and strict validation

Multi-environment Configuration ​

Load by Environment ​

go
// Determine config file based on environment
goEnv := os.Getenv("GO_ENV")
if goEnv == "" {
    goEnv = "development"
}

// Default: the first file loaded wins (keys already set by .env are
// not overridden by later files)
env.Load(".env", ".env."+goEnv, ".env.local")

// When you need last-file-wins (e.g. .env.local overrides .env):
cfg := env.DefaultConfig()
cfg.OverwriteExisting = true
cfg.Filenames = []string{".env", ".env." + goEnv, ".env.local"}
if err := env.LoadWithConfig(cfg); err != nil { log.Fatal(err) }

Overwrite semantics

Files loaded later do not override earlier ones by default: DefaultConfig() and ProductionConfig() both use first-file-wins, skipping keys that already exist to prevent accidentally overwriting loaded configuration at runtime. Set OverwriteExisting = true (built into the DevelopmentConfig()/TestingConfig() presets) for last-file-wins behavior.

Using a Loader Instance ​

When you need more control, use a Loader instance:

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	// Create configuration
	cfg := env.ProductionConfig()
	cfg.RequiredKeys = []string{"DB_HOST", "API_KEY"}

	// Create a loader
	loader, err := env.New(cfg)
	if err != nil {
		panic(err)
	}
	defer loader.Close()

	// Load files (in order; under ProductionConfig the first file wins, existing keys are not overwritten)
	if err := loader.LoadFiles(".env", ".env.production"); err != nil {
		panic(err)
	}

	// Validate required keys
	if err := loader.Validate(); err != nil {
		panic(err)
	}

	// Use
	host := loader.GetString("DB_HOST")
	fmt.Println("Host:", host)
}

Multi-file and Multi-format ​

Multi-file Loading ​

Files are loaded in order. By default (DefaultConfig()) the first file loaded wins: keys that already exist are skipped in later files. Set OverwriteExisting = true (built into the DevelopmentConfig()/TestingConfig() presets) if later files should override earlier ones:

go
env.Load(".env", "config.json", "config.yaml")
go
loader.LoadFiles(".env", ".env.local")

Multi-format Support ​

File format is auto-detected:

go
loader.LoadFiles("config.env", "settings.json", "secrets.yaml")
Supported formats
FormatExtensionDetection
.env.envFile extension
JSON.jsonFile extension
YAML.yaml, .ymlFile extension

Error Handling ​

go
import "errors"

err := loader.LoadFiles(".env")
if err != nil {
    switch {
    case errors.Is(err, env.ErrFileNotFound):
        // file not found
    case errors.Is(err, env.ErrFileTooLarge):
        // file too large
    case errors.Is(err, env.ErrSecurityViolation):
        // forbidden key (actually returns *SecurityError)
    default:
        // other errors
    }

    // Invalid key format: actually returns *ValidationError, Field=="key"
    var valErr *env.ValidationError
    if errors.As(err, &valErr) && valErr.Field == "key" {
        // invalid key format
    }
}
Getting detailed errors
go
// Parse error details
var parseErr *env.ParseError
if errors.As(err, &parseErr) {
    fmt.Printf("File %s line %d: %v\n", parseErr.File, parseErr.Line, parseErr.Err)
}

// File error details
var fileErr *env.FileError
if errors.As(err, &fileErr) {
    fmt.Printf("File %s operation %s failed: %v\n", fileErr.Path, fileErr.Op, fileErr.Err)
}

// Security error details
var secErr *env.SecurityError
if errors.As(err, &secErr) {
    fmt.Printf("Security error: %s - %s\n", secErr.Action, secErr.Reason)
}

Next Steps ​

Learn More ​

API Reference ​

Security ​