go / env
I read environment variables through small typed helpers with defaults.
Getters
package env
import (
"fmt"
"os"
"time"
)
// Require returns the value of key, or panics if it is unset or
// empty. Use for config the process can't start without.
func Require(key string) string {
if v := os.Getenv(key); v != "" {
return v
}
panic(fmt.Sprintf("missing required env var: %s", key))
}
// String returns the value of key, or defaultValue if unset/empty.
func String(key, defaultValue string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultValue
}
// Duration parses the value of key with time.ParseDuration.
// Unset, empty, or unparseable values fall back to defaultValue.
func Duration(key string, defaultValue time.Duration) time.Duration {
if v := os.Getenv(key); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
return defaultValue
}
return d
}
return defaultValue
}
.env loader
A small .env loader serves local development:
// Load walks up from the current working directory looking for a
// .env file and sets any variables not already present in the
// environment. Safe to call unconditionally: silent no-op when
// no .env is found.
func Load() {
dir, err := os.Getwd()
if err != nil {
return
}
for {
path := filepath.Join(dir, ".env")
if _, err := os.Stat(path); err == nil {
loadFile(path)
return
}
parent := filepath.Dir(dir)
if parent == dir {
return // reached filesystem root
}
dir = parent
}
}
// loadFile parses a .env-style file: one KEY=VALUE per line,
// # comments ignored, single- or multi-line quoted values
// supported. Existing env vars are never overwritten.
func loadFile(path string) {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close()
s := bufio.NewScanner(f)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" || line[0] == '#' {
continue
}
key, val, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
val = strings.TrimSpace(val)
if len(val) >= 1 && (val[0] == '"' || val[0] == '\'') {
quote := val[0]
if len(val) >= 2 && val[len(val)-1] == quote {
// single-line quoted
val = val[1 : len(val)-1]
} else {
// multi-line: accumulate until closing quote
var sb strings.Builder
sb.WriteString(val[1:])
for s.Scan() {
next := s.Text()
sb.WriteByte('\n')
if len(next) > 0 && next[len(next)-1] == quote {
sb.WriteString(next[:len(next)-1])
break
}
sb.WriteString(next)
}
val = sb.String()
}
}
if os.Getenv(key) == "" {
os.Setenv(key, val)
}
}
}
Development keeps secrets in a .env file. Production (e.g.
Render) sets them through the platform and has
no .env.
Load never overwrites a set variable, so the same binary serves both
without an APP_ENV variable.
Usage
main calls Load first:
func main() {
env.Load()
port := env.String("PORT", "8080")
timeout := env.Duration("TIMEOUT", 30*time.Second)
dbURL := env.Require("DATABASE_URL")
// ...
}