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")

	// ...
}

← All articles