A tiny, zero-dependency, security-first Go library to load environment variables from
.envfiles. A modern, actively maintained alternative to godotenv.
Keywords: golang dotenv, load .env file in Go, environment variables, 12-factor
config, godotenv alternative, secure env loader.
- Minimalist: Zero dependencies (standard library only), single file. Loads environment variables safely - nothing more, nothing less
- Secure by default: Following 12-factor, the real process environment always wins — a
.envfile never overrides existing variables unless you explicitly ask it to (Overload) - Robust: Comprehensive error handling with specific error types; files are applied atomically (a malformed file never leaves a half-applied environment)
- Correct parsing: POSIX-ish quoting — double quotes expand escapes, single quotes are literal, unquoted values support inline
# comments, optionalexportprefix, multi-line quoted values - Side-effect free option:
Parse/ParseBytesreturn amap[string]stringwithout ever touching the global environment — ideal for testing - Hardened: Configurable file-size limit to prevent memory exhaustion; no
bufio.Scanner64KB line cap
godotenv is great and battle-tested, but it
has been declared feature-complete and no longer accepts new functionality.
dotenv is a small, modern, actively-maintained alternative with safer
defaults.
jaavier/dotenv |
joho/godotenv |
|
|---|---|---|
| Dependencies | 0 (stdlib only) | 0 |
| Overrides existing env by default | No (safe; Overload to opt in) |
No (Load) / Yes (Overload) |
| Atomic apply (all-or-nothing per file) | Yes | No |
Side-effect-free Parse / ParseBytes |
Yes | Read returns a map |
Inline # comments (unquoted) |
Yes | Yes |
| Single-quote literal vs double-quote escapes | Yes | Yes |
| Multi-line quoted values (PEM keys) | Yes | Yes |
| Configurable max file size (DoS guard) | Yes | No |
| Long lines > 64 KB | Yes | Limited by Scanner |
Variable expansion ${VAR} |
No (by design — no injection surface) | Yes |
| Actively maintained | Yes | Feature-complete |
dotenvdeliberately omits${VAR}interpolation to keep the attack surface minimal. If you need interpolation, expand values yourself afterParse.
Parsing a representative .env (15 keys, comments, quotes, escapes) on a
commodity CPU:
BenchmarkParse-4 217534 ~4.6 µs/op 77 MB/s 3416 B/op 24 allocs/op
Run it yourself: make bench (or go test -bench=. -benchmem ./...).
go get github.com/jaavier/dotenv/v2Create a .env file in your project root:
API_KEY=your_secret_key
DB_HOST=localhost
DB_PORT=5432Load it in your Go application:
package main
import (
"log"
"os"
"github.com/jaavier/dotenv/v2"
)
func main() {
// Load default .env file. Existing environment variables are NOT
// overridden; the file only fills in what is missing.
if err := dotenv.Load(); err != nil {
log.Printf("Warning: %v", err)
}
// Use your environment variables
apiKey := os.Getenv("API_KEY")
dbHost := os.Getenv("DB_HOST")
}Secure default:
Loadnever overrides variables that already exist in the process environment. This matchesgodotenv/12-factor: the runtime is the source of truth and a stale or accidental.envcannot clobber injected secrets. UseOverloadwhen you explicitly want the file to win.
// Load multiple files (first file has priority)
err := dotenv.Load(".env.local", ".env")// Overload lets file values overwrite existing env vars (opt-in).
err := dotenv.Overload(".env")// Parse returns a map and never mutates the global environment.
vars, err := dotenv.Parse(reader) // any io.Reader
vars, err = dotenv.ParseBytes(data) // or raw bytes
fmt.Println(vars["API_KEY"])opts := &dotenv.Options{
Override: false, // default: do not override existing env vars
Required: true, // file must exist
MaxFileSize: 64 * 1024, // optional cap in bytes (0 => DefaultMaxFileSize)
}
err := dotenv.LoadWithOptions(opts, ".env.production")// Use MustLoad to panic if loading fails
dotenv.MustLoad(".env.required")// Simple get (same as os.Getenv)
apiKey := dotenv.Get("API_KEY")
// Get with default value
port := dotenv.GetOrDefault("PORT", "8080")
// Get required variable (panics if not set)
dbHost := dotenv.GetOrPanic("DB_HOST")# Full-line comments are supported
SIMPLE_KEY=value
# Inline comments (unquoted values only; '#' must follow whitespace)
PORT=8080 # the http port
PASSWORD=p#ss # '#' kept: not preceded by whitespace
# Optional leading 'export' (so the file can also be `source`d by a shell)
export API_KEY=secret
# Double quotes: escape sequences (\n \r \t \\ \") are expanded
MULTILINE="Line 1\nLine 2"
WITH_TAB="Column1\tColumn2"
# Single quotes: fully literal, no escapes, no comment stripping
LITERAL='value with \n kept as-is and a # too'
# Multi-line quoted values (e.g. PEM keys)
PRIVATE_KEY="-----BEGIN KEY-----
line2
-----END KEY-----"
# Empty values, and surrounding whitespace is trimmed on unquoted values
EMPTY_VALUE=
TRIMMED_VALUE= value with spaces trimmed The library provides specific error types for better error handling:
if err := dotenv.Load(".env"); err != nil {
switch {
case errors.Is(err, dotenv.ErrFileNotFound):
// File doesn't exist
case errors.Is(err, dotenv.ErrPermissionDenied):
// No permission to read file
case errors.Is(err, dotenv.ErrInvalidFormat):
// Invalid line format
case errors.Is(err, dotenv.ErrEmptyKey):
// Empty key found
case errors.Is(err, dotenv.ErrFileTooLarge):
// File exceeds the configured size limit
default:
// Other error
}
}- No-clobber default:
.envnever overrides existing process env vars (12-factor); overriding is opt-in viaOverload - Atomic apply: each file is fully parsed before any variable is set, so a parse error never leaves a partially-applied environment
- Side-effect-free parsing:
Parse/ParseBytesnever mutate the global environment - Resource limits: configurable file-size cap (default 1 MiB), enforced on the bytes actually read (safe for pipes/special files), with no 64KB line cap
- No code execution: command substitution (
$(...)) and shell evaluation are never performed - Key validation: only valid environment variable names (
[A-Za-z_][A-Za-z0-9_]*) are accepted
Loading Functions:
Load(filenames ...string) error- Load one or more .env files without overriding existing env varsOverload(filenames ...string) error- Load files, letting them override existing env varsLoadWithOptions(opts *Options, filenames ...string) error- Load with custom optionsMustLoad(filenames ...string)- Load files or panic
Parsing (no side effects):
Parse(r io.Reader) (map[string]string, error)- Parse into a map without touching the environmentParseBytes(data []byte) (map[string]string, error)- Convenience wrapper for in-memory data
Getting Variables:
Get(key string) string- Get environment variable value (alias for os.Getenv)GetOrDefault(key, defaultValue string) string- Get variable or return default if emptyGetOrPanic(key string) string- Get variable or panic if not set/empty
type Options struct {
Override bool // override existing environment variables (default false)
Required bool // file must exist (return error if not found)
MaxFileSize int64 // max bytes to read; <= 0 uses DefaultMaxFileSize
}
const DefaultMaxFileSize = 1 << 20 // 1 MiBErrFileNotFound- File does not existErrInvalidFormat- Invalid line format (missing=, unterminated quote, or garbage after a quote)ErrEmptyKey- Empty key nameErrPermissionDenied- No permission to read fileErrFileTooLarge- File exceeds the maximum size
How do I load a .env file in Go?
go get github.com/jaavier/dotenv/v2, then call dotenv.Load() at startup and read
values with os.Getenv (or dotenv.Get / GetOrDefault).
Does it override my existing environment variables?
No. Load only fills in variables that are not already set — the real
environment always wins. Use dotenv.Overload(...) if you want the file to win.
Is it a drop-in replacement for godotenv?
The API differs, but migration is trivial: godotenv.Load → dotenv.Load,
godotenv.Overload → dotenv.Overload, godotenv.Read → dotenv.Parse.
The main intentional difference is that ${VAR} interpolation is not performed.
Does it support variable expansion like ${OTHER}?
No, by design — this avoids an injection surface. Expand values yourself after
calling Parse if you need it.
Can I parse a string or stream without touching the environment?
Yes: dotenv.Parse(io.Reader) and dotenv.ParseBytes([]byte) return a map and
never mutate global state.
How do I upgrade from v1?
Update the import path to github.com/jaavier/dotenv/v2 and run
go get github.com/jaavier/dotenv/v2. The package name and all function
signatures are unchanged; the only behavior change is that Load no longer
overrides existing environment variables by default (use Overload for the old
behavior).
Which Go versions are supported? Go 1.17 and newer (tested on Linux, macOS and Windows in CI).
If this package is useful to you, please consider giving it a ⭐ on GitHub — it genuinely helps others discover it.
- Minimalista: Cero dependencias (solo librería estándar), un único archivo. Carga variables de entorno de manera segura - nada más, nada menos
- Seguro por defecto: Siguiendo 12-factor, el entorno real del proceso siempre gana — un archivo
.envnunca sobrescribe variables existentes a menos que lo pidas explícitamente (Overload) - Robusto: Manejo completo de errores con tipos específicos; los archivos se aplican de forma atómica (un archivo malformado nunca deja el entorno aplicado a medias)
- Análisis correcto: Comillas estilo POSIX — las comillas dobles expanden escapes, las simples son literales, los valores sin comillas soportan comentarios
# en línea, prefijoexportopcional y valores multilínea entre comillas - Opción sin efectos secundarios:
Parse/ParseBytesdevuelven unmap[string]stringsin tocar nunca el entorno global — ideal para tests - Endurecido: Límite de tamaño de archivo configurable para prevenir agotamiento de memoria; sin el límite de 64KB por línea de
bufio.Scanner
go get github.com/jaavier/dotenv/v2Crea un archivo .env en la raíz de tu proyecto:
API_KEY=tu_clave_secreta
DB_HOST=localhost
DB_PORT=5432Cárgalo en tu aplicación Go:
package main
import (
"log"
"os"
"github.com/jaavier/dotenv/v2"
)
func main() {
// Cargar archivo .env por defecto. Las variables de entorno existentes
// NO se sobrescriben; el archivo solo rellena lo que falta.
if err := dotenv.Load(); err != nil {
log.Printf("Advertencia: %v", err)
}
// Usar tus variables de entorno
apiKey := os.Getenv("API_KEY")
dbHost := os.Getenv("DB_HOST")
}Default seguro:
Loadnunca sobrescribe variables que ya existen en el entorno del proceso. Esto coincide congodotenv/12-factor: el runtime es la fuente de verdad y un.envaccidental o desactualizado no puede pisar secretos inyectados. UsaOverloadcuando quieras que el archivo gane.
// Cargar múltiples archivos (el primer archivo tiene prioridad)
err := dotenv.Load(".env.local", ".env")// Overload permite que los valores del archivo pisen las variables existentes.
err := dotenv.Overload(".env")// Parse devuelve un map y nunca muta el entorno global.
vars, err := dotenv.Parse(reader) // cualquier io.Reader
vars, err = dotenv.ParseBytes(data) // o bytes en crudo
fmt.Println(vars["API_KEY"])opts := &dotenv.Options{
Override: false, // por defecto: no sobrescribir variables existentes
Required: true, // el archivo debe existir
MaxFileSize: 64 * 1024, // límite opcional en bytes (0 => DefaultMaxFileSize)
}
err := dotenv.LoadWithOptions(opts, ".env.production")// Usar MustLoad para hacer panic si la carga falla
dotenv.MustLoad(".env.required")// Obtener simple (igual que os.Getenv)
apiKey := dotenv.Get("API_KEY")
// Obtener con valor por defecto
port := dotenv.GetOrDefault("PORT", "8080")
// Obtener variable requerida (hace panic si no está definida)
dbHost := dotenv.GetOrPanic("DB_HOST")# Comentarios de línea completa soportados
CLAVE_SIMPLE=valor
# Comentarios en línea (solo valores sin comillas; '#' debe seguir a un espacio)
PUERTO=8080 # el puerto http
PASSWORD=p#ss # '#' conservado: no va precedido de espacio
# Prefijo 'export' opcional (para que el archivo también pueda `source`arse)
export API_KEY=secreto
# Comillas dobles: se expanden los escapes (\n \r \t \\ \")
MULTILINEA="Línea 1\nLínea 2"
CON_TAB="Columna1\tColumna2"
# Comillas simples: totalmente literal, sin escapes ni comentarios
LITERAL='valor con \n tal cual y un # también'
# Valores multilínea entre comillas (p. ej. claves PEM)
PRIVATE_KEY="-----BEGIN KEY-----
line2
-----END KEY-----"
# Valores vacíos; los espacios alrededor se recortan en valores sin comillas
VALOR_VACIO=
VALOR_LIMPIO= valor con espacios eliminados La librería proporciona tipos de error específicos para un mejor manejo:
if err := dotenv.Load(".env"); err != nil {
switch {
case errors.Is(err, dotenv.ErrFileNotFound):
// El archivo no existe
case errors.Is(err, dotenv.ErrPermissionDenied):
// Sin permisos para leer el archivo
case errors.Is(err, dotenv.ErrInvalidFormat):
// Formato de línea inválido
case errors.Is(err, dotenv.ErrEmptyKey):
// Se encontró una clave vacía
case errors.Is(err, dotenv.ErrFileTooLarge):
// El archivo excede el límite de tamaño configurado
default:
// Otro error
}
}- Default sin sobrescritura: el
.envnunca pisa variables existentes del proceso (12-factor); sobrescribir es explícito conOverload - Aplicación atómica: cada archivo se analiza por completo antes de fijar ninguna variable, así un error de formato nunca deja el entorno a medias
- Análisis sin efectos secundarios:
Parse/ParseBytesnunca mutan el entorno global - Límites de recursos: tope de tamaño de archivo configurable (1 MiB por defecto), aplicado sobre los bytes realmente leídos (seguro para pipes/archivos especiales), sin límite de 64KB por línea
- Sin ejecución de código: nunca se realiza sustitución de comandos (
$(...)) ni evaluación de shell - Validación de claves: solo se aceptan nombres válidos de variables (
[A-Za-z_][A-Za-z0-9_]*)
Funciones de Carga:
Load(filenames ...string) error- Cargar uno o más archivos .env sin sobrescribir variables existentesOverload(filenames ...string) error- Cargar archivos dejando que sobrescriban las variables existentesLoadWithOptions(opts *Options, filenames ...string) error- Cargar con opciones personalizadasMustLoad(filenames ...string)- Cargar archivos o hacer panic
Análisis (sin efectos secundarios):
Parse(r io.Reader) (map[string]string, error)- Analizar a un map sin tocar el entornoParseBytes(data []byte) (map[string]string, error)- Atajo para datos en memoria
Funciones para Obtener Variables:
Get(key string) string- Obtener valor de variable de entorno (alias de os.Getenv)GetOrDefault(key, defaultValue string) string- Obtener variable o retornar valor por defecto si está vacíaGetOrPanic(key string) string- Obtener variable o hacer panic si no está definida/vacía
type Options struct {
Override bool // sobrescribir variables existentes (por defecto false)
Required bool // el archivo debe existir (retorna error si no)
MaxFileSize int64 // máximo de bytes a leer; <= 0 usa DefaultMaxFileSize
}
const DefaultMaxFileSize = 1 << 20 // 1 MiBErrFileNotFound- El archivo no existeErrInvalidFormat- Formato inválido (falta=, comilla sin cerrar o basura tras una comilla)ErrEmptyKey- Nombre de clave vacíoErrPermissionDenied- Sin permisos para leer el archivoErrFileTooLarge- El archivo excede el tamaño máximo
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details