Tools: goenv: Taming .env Files in Your DevOps Pipeline - Full Analysis

Tools: goenv: Taming .env Files in Your DevOps Pipeline - Full Analysis

The Problem With .env Files in Pipelines

What goenv Actually Is

Installation

CLI Tool

Go Package (env only, no CLI required)

Scenario 1: Bootstrapping a Fresh Service Environment

Scenario 2: Validation Gate Before Deployment

Scenario 3: Multi-Format Export for Infrastructure Tools

Scenario 4: Cleaning Up After a Pipeline Run

Scenario 5: Production File Protection

Scenario 6: n8n Automation Deployment

Scenario 7: Environment Promotion (Dev → Staging → Prod)

Scenario 8: GitHub Actions / GitLab CI Integration

GitHub Actions

GitLab CI

Scenario 9: Using the env Package in a Go Service

Scenario 10: Typed Validation in Application Startup

Scenario 11: Custom Separators for Non-Standard Formats

Scenario 12: Set, Unset, WasSet, WasUnset in Application Logic

Scenario 13: Controlling Package Behavior for Different Environments

Complete CLI Flag Reference

Complete env Package Function Reference

Existence and Truthiness

Mutation

Numeric Validation

Collection Validation

System

Key Behavioral Details Worth Knowing

Closing Thoughts How a lightweight Go tool and companion package eliminated the fragile bash gymnastics we'd been writing for years. Every team has the same archaeology story. Someone wrote a deployment script three years ago that does something like this: Then someone else needed to handle quoted values, so it became: Then a third person needed to check whether a variable existed before deploying, and now you have forty lines of bash doing something that should take one. The file accumulates. Nobody touches it. It works until it doesn't. goenv by Andrei Merlescu solves this with two things: a compiled CLI binary you can drop into any pipeline, and a typed Go package for applications that need to read env vars with real type safety and validation. This article walks through both — from initial setup through production pipeline automation — using real scenarios drawn from actual DevOps workflows. Before diving in, it helps to understand the two distinct components: The goenv CLI is a binary that lets you create, read, modify, validate, and export .env files into other formats (JSON, YAML, TOML, INI, XML). It is designed to be composed in shell scripts and CI pipelines. The env package (github.com/andreimerlescu/goenv/env) is a Go library for reading typed environment variables from the process environment — booleans, integers, durations, lists, maps — with fallbacks, validation helpers, and configurable parsing behavior. They share a common philosophy: be explicit, be composable, fail loudly when something is wrong. In a CI environment where you cannot rely on go install, build and cache the binary: The repository ships pre-built binaries for darwin-amd64, darwin-arm64, linux-amd64, and windows in its bin/ directory, which you can copy directly into your pipeline image. Your team is deploying a new microservice. The deployment script needs to create the .env file from scratch, populate it with runtime values, and validate it before the service starts. Previously this was done with a heredoc and sed. Now: Notice the key discipline here: -add only adds a key if it does not already exist. If you run this script twice, it does not overwrite DEPLOY_SHA with a different commit hash on the second run. The existing value is preserved. This is intentional and important for idempotent pipelines. Your CD pipeline has a validation stage that runs before any deployment. It needs to confirm that a staging .env file contains the expected values — not just that the keys exist, but that they hold the right content. The -is flag handles this: This pattern makes validation failures explicit and human-readable. Each check carries its own error message. The pipeline log tells you exactly what failed and why, rather than leaving you to diff files manually. Your deployment touches three different systems: a Terraform workspace that reads JSON, an Ansible playbook that reads INI, and a Helm values override that reads YAML. You maintain one .env file and derive the others: If you only need one format for a specific pipeline step, export just that one: You cannot combine format flags in a single call — goenv exits with an error if you try to use -json and -yaml together. This is by design: one command, one output format, one file. Use -mkall when you want all of them. Generated format files are build artifacts. They should not accumulate between runs. The -cleanall flag removes all derived format files while leaving the source .env intact: You can also prevent deletion entirely via environment variable, which is useful if a downstream job still needs the files when the cleanup hook fires: Production .env files warrant special treatment. goenv has a built-in protection mechanism: any file whose path contains production is treated as protected by default. Interacting with it requires explicitly passing -prod: For pipelines where you want to enforce this at the environment level and never allow any script to write production files regardless of flags: This is particularly useful in CI environments where you want a hard guarantee that no job — regardless of what flags it passes — can mutate a production config. This is a direct example from the goenv README. You are deploying an n8n instance and need to build its environment file from scratch with runtime values: You have three environment files. When promoting a release, you need to carry certain values forward, strip others, and enforce that the promoted file is valid before it is used: The CLI handles files. The env package handles your running process. Here is a realistic service configuration pattern: Beyond simply reading values, the env package can assert numeric constraints and list membership at startup — before the application accepts any traffic: Some systems export environment variables with non-comma separators. The env package lets you change list and map parsing at runtime, either in code or via environment variables: Or set them entirely from the environment without touching code — useful when the same binary needs to adapt to different upstream formats across environments: Sometimes your application needs to mutate its own environment — for instance, deriving a value at startup and making it available to child processes or subpackages: The env package ships with a Magic() function that reads AM_GO_ENV_* environment variables at startup and applies them automatically. This runs by default via the init() function. You can disable it and configure manually: When you want the opposite — maximum noise for debugging in a local dev environment — set these in your shell before running: Every call to env.String(), env.Int(), env.Bool() etc. will log to stdout when it falls back to a default value, giving you full visibility into what the process is and is not finding in its environment. -add is idempotent. It only writes if the key is absent. If you run your bootstrap script twice, the second run does not clobber the first. This is the behavior you want in any pipeline that might retry. -rm rewrites the file. It reads all lines, filters out the matching key, and writes the result back. It does not do in-place line editing. Format flags are mutually exclusive. Combining -json and -yaml in one call exits with an error. Use -mkall when you need all formats. -cleanall without -write is a dry run. It will print what it would delete but will not actually remove anything. The env package's Magic() runs at import time. If you import the package, AM_GO_ENV_* variables in your shell are picked up automatically before main() runs. Set env.UseMagic = false in your own init() if you want explicit control. MustExist respects AllowPanic. By default it calls os.Exit(1). Set AM_GO_ENV_ALWAYS_ALLOW_PANIC=true (or env.AllowPanic = true) to get a panic() instead — useful in test environments where you want the stack trace. The fragile bash patterns that accumulate around .env files are a symptom of tooling that was never designed for structured config management. goenv treats .env files as first-class data: readable, writable, queryable, exportable, and validatable through a consistent interface that composes cleanly into any pipeline. The CLI gives shell scripts and CI jobs a stable contract for interacting with env files. The env package gives Go applications a typed, validated, zero-dependency way to read their own environment. Together they cover the full lifecycle of an environment variable from the file it is defined in to the running process that uses it. The source is at github.com/andreimerlescu/goenv. The env sub-package is independently installable at github.com/andreimerlescu/goenv/env and carries an Apache 2.0 license. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

grep "^DB_HOST=" .env | cut -d= -f2 grep "^DB_HOST=" .env | cut -d= -f2 grep "^DB_HOST=" .env | cut -d= -f2 grep "^DB_HOST=" .env | cut -d= -f2 | tr -d '"' grep "^DB_HOST=" .env | cut -d= -f2 | tr -d '"' grep "^DB_HOST=" .env | cut -d= -f2 | tr -d '"' go install github.com/andreimerlescu/goenv@latest go install github.com/andreimerlescu/goenv@latest go install github.com/andreimerlescu/goenv@latest GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/goenv-linux-amd64 . GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/goenv-linux-amd64 . GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/goenv-linux-amd64 . go get -u github.com/andreimerlescu/goenv/env go get -u github.com/andreimerlescu/goenv/env go get -u github.com/andreimerlescu/goenv/env #!/bin/bash set -euo pipefail SERVICE_DIR="/opt/services/payments" ENV_FILE="${SERVICE_DIR}/service.env" # Create the file if it doesn't exist yet goenv -file "${ENV_FILE}" -init -write # Populate with values derived at deploy time goenv -file "${ENV_FILE}" -write -add -env SERVICE_NAME -value "payments" goenv -file "${ENV_FILE}" -write -add -env SERVICE_PORT -value "8443" goenv -file "${ENV_FILE}" -write -add -env DEPLOY_SHA -value "$(git rev-parse --short HEAD)" goenv -file "${ENV_FILE}" -write -add -env DEPLOY_DATE -value "$(date -u +%Y-%m-%dT%H:%M:%SZ)" goenv -file "${ENV_FILE}" -write -add -env DATA_FOLDER -value "$(pwd)/data" goenv -file "${ENV_FILE}" -write -add -env LOG_LEVEL -value "info" # Confirm everything landed goenv -file "${ENV_FILE}" -print # Validate required keys exist before handing off to the service goenv -file "${ENV_FILE}" -has -env SERVICE_NAME || { echo "SERVICE_NAME missing"; exit 1; } goenv -file "${ENV_FILE}" -has -env DEPLOY_SHA || { echo "DEPLOY_SHA missing"; exit 1; } echo "Environment bootstrapped successfully." #!/bin/bash set -euo pipefail SERVICE_DIR="/opt/services/payments" ENV_FILE="${SERVICE_DIR}/service.env" # Create the file if it doesn't exist yet goenv -file "${ENV_FILE}" -init -write # Populate with values derived at deploy time goenv -file "${ENV_FILE}" -write -add -env SERVICE_NAME -value "payments" goenv -file "${ENV_FILE}" -write -add -env SERVICE_PORT -value "8443" goenv -file "${ENV_FILE}" -write -add -env DEPLOY_SHA -value "$(git rev-parse --short HEAD)" goenv -file "${ENV_FILE}" -write -add -env DEPLOY_DATE -value "$(date -u +%Y-%m-%dT%H:%M:%SZ)" goenv -file "${ENV_FILE}" -write -add -env DATA_FOLDER -value "$(pwd)/data" goenv -file "${ENV_FILE}" -write -add -env LOG_LEVEL -value "info" # Confirm everything landed goenv -file "${ENV_FILE}" -print # Validate required keys exist before handing off to the service goenv -file "${ENV_FILE}" -has -env SERVICE_NAME || { echo "SERVICE_NAME missing"; exit 1; } goenv -file "${ENV_FILE}" -has -env DEPLOY_SHA || { echo "DEPLOY_SHA missing"; exit 1; } echo "Environment bootstrapped successfully." #!/bin/bash set -euo pipefail SERVICE_DIR="/opt/services/payments" ENV_FILE="${SERVICE_DIR}/service.env" # Create the file if it doesn't exist yet goenv -file "${ENV_FILE}" -init -write # Populate with values derived at deploy time goenv -file "${ENV_FILE}" -write -add -env SERVICE_NAME -value "payments" goenv -file "${ENV_FILE}" -write -add -env SERVICE_PORT -value "8443" goenv -file "${ENV_FILE}" -write -add -env DEPLOY_SHA -value "$(git rev-parse --short HEAD)" goenv -file "${ENV_FILE}" -write -add -env DEPLOY_DATE -value "$(date -u +%Y-%m-%dT%H:%M:%SZ)" goenv -file "${ENV_FILE}" -write -add -env DATA_FOLDER -value "$(pwd)/data" goenv -file "${ENV_FILE}" -write -add -env LOG_LEVEL -value "info" # Confirm everything landed goenv -file "${ENV_FILE}" -print # Validate required keys exist before handing off to the service goenv -file "${ENV_FILE}" -has -env SERVICE_NAME || { echo "SERVICE_NAME missing"; exit 1; } goenv -file "${ENV_FILE}" -has -env DEPLOY_SHA || { echo "DEPLOY_SHA missing"; exit 1; } echo "Environment bootstrapped successfully." #!/bin/bash set -euo pipefail ENV_FILE=".env.staging" echo "==> Validating staging environment..." # Assert APP_ENV equals "staging" if ! goenv -file "${ENV_FILE}" -is -env APP_ENV -value staging; then echo "ERROR: APP_ENV must be 'staging' in ${ENV_FILE}" exit 1 fi # Assert DEBUG is not enabled in staging if goenv -file "${ENV_FILE}" -is -env DEBUG -value true; then echo "ERROR: DEBUG must not be 'true' in staging" exit 1 fi # Assert a required key exists if ! goenv -file "${ENV_FILE}" -has -env DATABASE_URL; then echo "ERROR: DATABASE_URL is required" exit 1 fi # Assert a key that should have been removed is actually gone if ! goenv -file "${ENV_FILE}" -not -has -env LEGACY_API_KEY; then echo "ERROR: LEGACY_API_KEY should have been removed from ${ENV_FILE}" exit 1 fi # Assert DB_PORT is not pointing at a dev-only value if goenv -file "${ENV_FILE}" -is -env DB_PORT -value 5433; then echo "ERROR: DB_PORT 5433 is the dev port — use 5432 in staging" exit 1 fi echo "==> Validation passed." #!/bin/bash set -euo pipefail ENV_FILE=".env.staging" echo "==> Validating staging environment..." # Assert APP_ENV equals "staging" if ! goenv -file "${ENV_FILE}" -is -env APP_ENV -value staging; then echo "ERROR: APP_ENV must be 'staging' in ${ENV_FILE}" exit 1 fi # Assert DEBUG is not enabled in staging if goenv -file "${ENV_FILE}" -is -env DEBUG -value true; then echo "ERROR: DEBUG must not be 'true' in staging" exit 1 fi # Assert a required key exists if ! goenv -file "${ENV_FILE}" -has -env DATABASE_URL; then echo "ERROR: DATABASE_URL is required" exit 1 fi # Assert a key that should have been removed is actually gone if ! goenv -file "${ENV_FILE}" -not -has -env LEGACY_API_KEY; then echo "ERROR: LEGACY_API_KEY should have been removed from ${ENV_FILE}" exit 1 fi # Assert DB_PORT is not pointing at a dev-only value if goenv -file "${ENV_FILE}" -is -env DB_PORT -value 5433; then echo "ERROR: DB_PORT 5433 is the dev port — use 5432 in staging" exit 1 fi echo "==> Validation passed." #!/bin/bash set -euo pipefail ENV_FILE=".env.staging" echo "==> Validating staging environment..." # Assert APP_ENV equals "staging" if ! goenv -file "${ENV_FILE}" -is -env APP_ENV -value staging; then echo "ERROR: APP_ENV must be 'staging' in ${ENV_FILE}" exit 1 fi # Assert DEBUG is not enabled in staging if goenv -file "${ENV_FILE}" -is -env DEBUG -value true; then echo "ERROR: DEBUG must not be 'true' in staging" exit 1 fi # Assert a required key exists if ! goenv -file "${ENV_FILE}" -has -env DATABASE_URL; then echo "ERROR: DATABASE_URL is required" exit 1 fi # Assert a key that should have been removed is actually gone if ! goenv -file "${ENV_FILE}" -not -has -env LEGACY_API_KEY; then echo "ERROR: LEGACY_API_KEY should have been removed from ${ENV_FILE}" exit 1 fi # Assert DB_PORT is not pointing at a dev-only value if goenv -file "${ENV_FILE}" -is -env DB_PORT -value 5433; then echo "ERROR: DB_PORT 5433 is the dev port — use 5432 in staging" exit 1 fi echo "==> Validation passed." #!/bin/bash set -euo pipefail ENV_FILE="infra/deploy.env" echo "==> Exporting ${ENV_FILE} to all formats..." # Generate all formats in one command goenv -file "${ENV_FILE}" -mkall -write # Resulting files: # infra/deploy.env.json ← Terraform input # infra/deploy.env.yaml ← Helm values # infra/deploy.env.ini ← Ansible inventory vars # infra/deploy.env.toml ← any TOML-native tooling # infra/deploy.env.xml ← any XML-native tooling ls -lh infra/deploy.env* # Verify the JSON is valid before passing it to Terraform cat infra/deploy.env.json | python3 -m json.tool > /dev/null \ && echo "JSON valid" \ || { echo "JSON invalid"; exit 1; } echo "==> Export complete." #!/bin/bash set -euo pipefail ENV_FILE="infra/deploy.env" echo "==> Exporting ${ENV_FILE} to all formats..." # Generate all formats in one command goenv -file "${ENV_FILE}" -mkall -write # Resulting files: # infra/deploy.env.json ← Terraform input # infra/deploy.env.yaml ← Helm values # infra/deploy.env.ini ← Ansible inventory vars # infra/deploy.env.toml ← any TOML-native tooling # infra/deploy.env.xml ← any XML-native tooling ls -lh infra/deploy.env* # Verify the JSON is valid before passing it to Terraform cat infra/deploy.env.json | python3 -m json.tool > /dev/null \ && echo "JSON valid" \ || { echo "JSON invalid"; exit 1; } echo "==> Export complete." #!/bin/bash set -euo pipefail ENV_FILE="infra/deploy.env" echo "==> Exporting ${ENV_FILE} to all formats..." # Generate all formats in one command goenv -file "${ENV_FILE}" -mkall -write # Resulting files: # infra/deploy.env.json ← Terraform input # infra/deploy.env.yaml ← Helm values # infra/deploy.env.ini ← Ansible inventory vars # infra/deploy.env.toml ← any TOML-native tooling # infra/deploy.env.xml ← any XML-native tooling ls -lh infra/deploy.env* # Verify the JSON is valid before passing it to Terraform cat infra/deploy.env.json | python3 -m json.tool > /dev/null \ && echo "JSON valid" \ || { echo "JSON invalid"; exit 1; } echo "==> Export complete." # Jenkins pipeline step: export for Ansible only goenv -file deploy.env -ini -write # GitLab CI step: export for Helm only goenv -file deploy.env -yaml -write # Jenkins pipeline step: export for Ansible only goenv -file deploy.env -ini -write # GitLab CI step: export for Helm only goenv -file deploy.env -yaml -write # Jenkins pipeline step: export for Ansible only goenv -file deploy.env -ini -write # GitLab CI step: export for Helm only goenv -file deploy.env -yaml -write #!/bin/bash # cleanup.sh — run as a post-pipeline hook ENV_FILE="deploy.env" echo "==> Cleaning derived format files..." goenv -file "${ENV_FILE}" -cleanall -write # Only deploy.env remains; .json .yaml .toml .ini .xml are removed ls -la *.env* echo "==> Clean complete." #!/bin/bash # cleanup.sh — run as a post-pipeline hook ENV_FILE="deploy.env" echo "==> Cleaning derived format files..." goenv -file "${ENV_FILE}" -cleanall -write # Only deploy.env remains; .json .yaml .toml .ini .xml are removed ls -la *.env* echo "==> Clean complete." #!/bin/bash # cleanup.sh — run as a post-pipeline hook ENV_FILE="deploy.env" echo "==> Cleaning derived format files..." goenv -file "${ENV_FILE}" -cleanall -write # Only deploy.env remains; .json .yaml .toml .ini .xml are removed ls -la *.env* echo "==> Clean complete." export AM_GO_ENV_NEVER_DELETE=true goenv -file deploy.env -cleanall -write # → no files deleted; flag is silently respected export AM_GO_ENV_NEVER_DELETE=true goenv -file deploy.env -cleanall -write # → no files deleted; flag is silently respected export AM_GO_ENV_NEVER_DELETE=true goenv -file deploy.env -cleanall -write # → no files deleted; flag is silently respected # This exits with an error — no -prod flag goenv -file .env.production -write -add -env SECRET_KEY -value "new-secret" # This works — -prod flag grants access goenv -prod -file .env.production -write -add -env SECRET_KEY -value "new-secret" # This exits with an error — no -prod flag goenv -file .env.production -write -add -env SECRET_KEY -value "new-secret" # This works — -prod flag grants access goenv -prod -file .env.production -write -add -env SECRET_KEY -value "new-secret" # This exits with an error — no -prod flag goenv -file .env.production -write -add -env SECRET_KEY -value "new-secret" # This works — -prod flag grants access goenv -prod -file .env.production -write -add -env SECRET_KEY -value "new-secret" export GOENV_NEVER_WRITE_PRODUCTION=true # Even with -prod, writes are blocked goenv -prod -file .env.production -write -add -env SECRET_KEY -value "x" # → exits with error export GOENV_NEVER_WRITE_PRODUCTION=true # Even with -prod, writes are blocked goenv -prod -file .env.production -write -add -env SECRET_KEY -value "x" # → exits with error export GOENV_NEVER_WRITE_PRODUCTION=true # Even with -prod, writes are blocked goenv -prod -file .env.production -write -add -env SECRET_KEY -value "x" # → exits with error #!/bin/bash set -euo pipefail ENV_FILE="n8n.env" DOMAIN="gh.dev" SUBDOMAIN="n8n" goenv -init -write -file "${ENV_FILE}" goenv -write -file "${ENV_FILE}" -add -env DATA_FOLDER -value "$(pwd)" goenv -write -file "${ENV_FILE}" -add -env DOMAIN -value "${DOMAIN}" goenv -write -file "${ENV_FILE}" -add -env SUBDOMAIN -value "${SUBDOMAIN}" goenv -write -file "${ENV_FILE}" -add -env SSL_EMAIL -value "webmaster@${SUBDOMAIN}.${DOMAIN}" # Inspect before deploying goenv -file "${ENV_FILE}" -print # Validate the SSL email was formed correctly goenv -file "${ENV_FILE}" -is -env SSL_EMAIL -value "[email protected]" \ && echo "SSL_EMAIL correct" \ || { echo "SSL_EMAIL malformed"; exit 1; } # Check GENERIC_TIMEZONE — expected to be absent until set if goenv -file "${ENV_FILE}" -not -has -env GENERIC_TIMEZONE; then echo "WARNING: GENERIC_TIMEZONE not set — n8n will default to UTC" fi # Export for review as TOML and XML goenv -file "${ENV_FILE}" -toml goenv -file "${ENV_FILE}" -xml #!/bin/bash set -euo pipefail ENV_FILE="n8n.env" DOMAIN="gh.dev" SUBDOMAIN="n8n" goenv -init -write -file "${ENV_FILE}" goenv -write -file "${ENV_FILE}" -add -env DATA_FOLDER -value "$(pwd)" goenv -write -file "${ENV_FILE}" -add -env DOMAIN -value "${DOMAIN}" goenv -write -file "${ENV_FILE}" -add -env SUBDOMAIN -value "${SUBDOMAIN}" goenv -write -file "${ENV_FILE}" -add -env SSL_EMAIL -value "webmaster@${SUBDOMAIN}.${DOMAIN}" # Inspect before deploying goenv -file "${ENV_FILE}" -print # Validate the SSL email was formed correctly goenv -file "${ENV_FILE}" -is -env SSL_EMAIL -value "[email protected]" \ && echo "SSL_EMAIL correct" \ || { echo "SSL_EMAIL malformed"; exit 1; } # Check GENERIC_TIMEZONE — expected to be absent until set if goenv -file "${ENV_FILE}" -not -has -env GENERIC_TIMEZONE; then echo "WARNING: GENERIC_TIMEZONE not set — n8n will default to UTC" fi # Export for review as TOML and XML goenv -file "${ENV_FILE}" -toml goenv -file "${ENV_FILE}" -xml #!/bin/bash set -euo pipefail ENV_FILE="n8n.env" DOMAIN="gh.dev" SUBDOMAIN="n8n" goenv -init -write -file "${ENV_FILE}" goenv -write -file "${ENV_FILE}" -add -env DATA_FOLDER -value "$(pwd)" goenv -write -file "${ENV_FILE}" -add -env DOMAIN -value "${DOMAIN}" goenv -write -file "${ENV_FILE}" -add -env SUBDOMAIN -value "${SUBDOMAIN}" goenv -write -file "${ENV_FILE}" -add -env SSL_EMAIL -value "webmaster@${SUBDOMAIN}.${DOMAIN}" # Inspect before deploying goenv -file "${ENV_FILE}" -print # Validate the SSL email was formed correctly goenv -file "${ENV_FILE}" -is -env SSL_EMAIL -value "[email protected]" \ && echo "SSL_EMAIL correct" \ || { echo "SSL_EMAIL malformed"; exit 1; } # Check GENERIC_TIMEZONE — expected to be absent until set if goenv -file "${ENV_FILE}" -not -has -env GENERIC_TIMEZONE; then echo "WARNING: GENERIC_TIMEZONE not set — n8n will default to UTC" fi # Export for review as TOML and XML goenv -file "${ENV_FILE}" -toml goenv -file "${ENV_FILE}" -xml #!/bin/bash set -euo pipefail SOURCE=".env.development" TARGET=".env.staging" echo "==> Promoting ${SOURCE} to ${TARGET}..." # Read values from dev and write into staging # (goenv -add will not overwrite if key already exists in staging) for KEY in APP_NAME APP_VERSION DEPLOY_SHA LOG_LEVEL; do VALUE=$(goenv -file "${SOURCE}" -is -env "${KEY}" -value "" || true) goenv -file "${TARGET}" -write -add -env "${KEY}" -value "${VALUE}" done # Scrub dev-only keys from the staging file goenv -file "${TARGET}" -write -rm -env LOCAL_DB_PATH goenv -file "${TARGET}" -write -rm -env DEV_MOCK_PAYMENTS goenv -file "${TARGET}" -write -rm -env SKIP_AUTH # Confirm dev-only keys are absent goenv -file "${TARGET}" -not -has -env LOCAL_DB_PATH || { echo "LOCAL_DB_PATH still present"; exit 1; } goenv -file "${TARGET}" -not -has -env DEV_MOCK_PAYMENTS || { echo "DEV_MOCK_PAYMENTS still present"; exit 1; } # Assert staging-specific values are correct goenv -file "${TARGET}" -is -env APP_ENV -value staging || { echo "APP_ENV must be staging"; exit 1; } echo "==> Promotion complete." goenv -file "${TARGET}" -print #!/bin/bash set -euo pipefail SOURCE=".env.development" TARGET=".env.staging" echo "==> Promoting ${SOURCE} to ${TARGET}..." # Read values from dev and write into staging # (goenv -add will not overwrite if key already exists in staging) for KEY in APP_NAME APP_VERSION DEPLOY_SHA LOG_LEVEL; do VALUE=$(goenv -file "${SOURCE}" -is -env "${KEY}" -value "" || true) goenv -file "${TARGET}" -write -add -env "${KEY}" -value "${VALUE}" done # Scrub dev-only keys from the staging file goenv -file "${TARGET}" -write -rm -env LOCAL_DB_PATH goenv -file "${TARGET}" -write -rm -env DEV_MOCK_PAYMENTS goenv -file "${TARGET}" -write -rm -env SKIP_AUTH # Confirm dev-only keys are absent goenv -file "${TARGET}" -not -has -env LOCAL_DB_PATH || { echo "LOCAL_DB_PATH still present"; exit 1; } goenv -file "${TARGET}" -not -has -env DEV_MOCK_PAYMENTS || { echo "DEV_MOCK_PAYMENTS still present"; exit 1; } # Assert staging-specific values are correct goenv -file "${TARGET}" -is -env APP_ENV -value staging || { echo "APP_ENV must be staging"; exit 1; } echo "==> Promotion complete." goenv -file "${TARGET}" -print #!/bin/bash set -euo pipefail SOURCE=".env.development" TARGET=".env.staging" echo "==> Promoting ${SOURCE} to ${TARGET}..." # Read values from dev and write into staging # (goenv -add will not overwrite if key already exists in staging) for KEY in APP_NAME APP_VERSION DEPLOY_SHA LOG_LEVEL; do VALUE=$(goenv -file "${SOURCE}" -is -env "${KEY}" -value "" || true) goenv -file "${TARGET}" -write -add -env "${KEY}" -value "${VALUE}" done # Scrub dev-only keys from the staging file goenv -file "${TARGET}" -write -rm -env LOCAL_DB_PATH goenv -file "${TARGET}" -write -rm -env DEV_MOCK_PAYMENTS goenv -file "${TARGET}" -write -rm -env SKIP_AUTH # Confirm dev-only keys are absent goenv -file "${TARGET}" -not -has -env LOCAL_DB_PATH || { echo "LOCAL_DB_PATH still present"; exit 1; } goenv -file "${TARGET}" -not -has -env DEV_MOCK_PAYMENTS || { echo "DEV_MOCK_PAYMENTS still present"; exit 1; } # Assert staging-specific values are correct goenv -file "${TARGET}" -is -env APP_ENV -value staging || { echo "APP_ENV must be staging"; exit 1; } echo "==> Promotion complete." goenv -file "${TARGET}" -print name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Install goenv run: go install github.com/andreimerlescu/goenv@latest - name: Validate environment file run: | goenv -file .env.staging -has -env DATABASE_URL || exit 1 goenv -file .env.staging -has -env SERVICE_PORT || exit 1 goenv -file .env.staging -not -has -env DEBUG_KEY || exit 1 goenv -file .env.staging -is -env APP_ENV -value staging || exit 1 - name: Export to JSON for Terraform run: | goenv -file .env.staging -json -write cat .env.staging.json - name: Deploy run: ./scripts/deploy.sh - name: Clean up artifacts if: always() run: goenv -file .env.staging -cleanall -write name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Install goenv run: go install github.com/andreimerlescu/goenv@latest - name: Validate environment file run: | goenv -file .env.staging -has -env DATABASE_URL || exit 1 goenv -file .env.staging -has -env SERVICE_PORT || exit 1 goenv -file .env.staging -not -has -env DEBUG_KEY || exit 1 goenv -file .env.staging -is -env APP_ENV -value staging || exit 1 - name: Export to JSON for Terraform run: | goenv -file .env.staging -json -write cat .env.staging.json - name: Deploy run: ./scripts/deploy.sh - name: Clean up artifacts if: always() run: goenv -file .env.staging -cleanall -write name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Install goenv run: go install github.com/andreimerlescu/goenv@latest - name: Validate environment file run: | goenv -file .env.staging -has -env DATABASE_URL || exit 1 goenv -file .env.staging -has -env SERVICE_PORT || exit 1 goenv -file .env.staging -not -has -env DEBUG_KEY || exit 1 goenv -file .env.staging -is -env APP_ENV -value staging || exit 1 - name: Export to JSON for Terraform run: | goenv -file .env.staging -json -write cat .env.staging.json - name: Deploy run: ./scripts/deploy.sh - name: Clean up artifacts if: always() run: goenv -file .env.staging -cleanall -write stages: - validate - export - deploy - cleanup validate_env: stage: validate image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -has -env DB_HOST || exit 1 - goenv -file .env.production -has -env REDIS_URL || exit 1 - goenv -file .env.production -not -has -env DEV_FLAG || exit 1 - goenv -file .env.production -is -env APP_ENV -value production || exit 1 export_formats: stage: export image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -mkall -write artifacts: paths: - .env.production.json - .env.production.yaml cleanup: stage: cleanup image: golang:1.22 when: always script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -cleanall -write stages: - validate - export - deploy - cleanup validate_env: stage: validate image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -has -env DB_HOST || exit 1 - goenv -file .env.production -has -env REDIS_URL || exit 1 - goenv -file .env.production -not -has -env DEV_FLAG || exit 1 - goenv -file .env.production -is -env APP_ENV -value production || exit 1 export_formats: stage: export image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -mkall -write artifacts: paths: - .env.production.json - .env.production.yaml cleanup: stage: cleanup image: golang:1.22 when: always script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -cleanall -write stages: - validate - export - deploy - cleanup validate_env: stage: validate image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -has -env DB_HOST || exit 1 - goenv -file .env.production -has -env REDIS_URL || exit 1 - goenv -file .env.production -not -has -env DEV_FLAG || exit 1 - goenv -file .env.production -is -env APP_ENV -value production || exit 1 export_formats: stage: export image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -mkall -write artifacts: paths: - .env.production.json - .env.production.yaml cleanup: stage: cleanup image: golang:1.22 when: always script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -cleanall -write package main import ( "fmt" "log" "net/http" "time" "github.com/andreimerlescu/goenv/env" ) type Config struct { Host string Port int Debug bool Timeout time.Duration MaxConns int AllowedOrigins []string DBCredentials map[string]string } func LoadConfig() Config { // MustExist exits (or panics) if these are absent — // use in init() to catch misconfiguration at startup env.MustExist("DB_HOST") env.MustExist("DB_PASS") return Config{ Host: env.String("HOST", "0.0.0.0"), Port: env.Int("PORT", 8080), Debug: env.Bool("DEBUG", false), Timeout: env.Duration("REQUEST_TIMEOUT", 30*time.Second), MaxConns: env.Int("DB_MAX_CONNS", 10), // ALLOWED_ORIGINS="https://a.com,https://b.com" AllowedOrigins: env.List("ALLOWED_ORIGINS", env.ZeroList), // DB_CREDENTIALS="host=localhost,port=5432,name=mydb" DBCredentials: env.Map("DB_CREDENTIALS", env.ZeroMap), } } func main() { cfg := LoadConfig() addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) log.Printf("starting on %s (debug=%v timeout=%s)", addr, cfg.Debug, cfg.Timeout) http.ListenAndServe(addr, nil) } package main import ( "fmt" "log" "net/http" "time" "github.com/andreimerlescu/goenv/env" ) type Config struct { Host string Port int Debug bool Timeout time.Duration MaxConns int AllowedOrigins []string DBCredentials map[string]string } func LoadConfig() Config { // MustExist exits (or panics) if these are absent — // use in init() to catch misconfiguration at startup env.MustExist("DB_HOST") env.MustExist("DB_PASS") return Config{ Host: env.String("HOST", "0.0.0.0"), Port: env.Int("PORT", 8080), Debug: env.Bool("DEBUG", false), Timeout: env.Duration("REQUEST_TIMEOUT", 30*time.Second), MaxConns: env.Int("DB_MAX_CONNS", 10), // ALLOWED_ORIGINS="https://a.com,https://b.com" AllowedOrigins: env.List("ALLOWED_ORIGINS", env.ZeroList), // DB_CREDENTIALS="host=localhost,port=5432,name=mydb" DBCredentials: env.Map("DB_CREDENTIALS", env.ZeroMap), } } func main() { cfg := LoadConfig() addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) log.Printf("starting on %s (debug=%v timeout=%s)", addr, cfg.Debug, cfg.Timeout) http.ListenAndServe(addr, nil) } package main import ( "fmt" "log" "net/http" "time" "github.com/andreimerlescu/goenv/env" ) type Config struct { Host string Port int Debug bool Timeout time.Duration MaxConns int AllowedOrigins []string DBCredentials map[string]string } func LoadConfig() Config { // MustExist exits (or panics) if these are absent — // use in init() to catch misconfiguration at startup env.MustExist("DB_HOST") env.MustExist("DB_PASS") return Config{ Host: env.String("HOST", "0.0.0.0"), Port: env.Int("PORT", 8080), Debug: env.Bool("DEBUG", false), Timeout: env.Duration("REQUEST_TIMEOUT", 30*time.Second), MaxConns: env.Int("DB_MAX_CONNS", 10), // ALLOWED_ORIGINS="https://a.com,https://b.com" AllowedOrigins: env.List("ALLOWED_ORIGINS", env.ZeroList), // DB_CREDENTIALS="host=localhost,port=5432,name=mydb" DBCredentials: env.Map("DB_CREDENTIALS", env.ZeroMap), } } func main() { cfg := LoadConfig() addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) log.Printf("starting on %s (debug=%v timeout=%s)", addr, cfg.Debug, cfg.Timeout) http.ListenAndServe(addr, nil) } package main import ( "fmt" "os" "github.com/andreimerlescu/goenv/env" ) func validateEnvironment() { errors := []string{} // Port must be in user range if !env.IntInRange("PORT", 8080, 1024, 49151) { errors = append(errors, "PORT must be between 1024 and 49151") } // Max connections must not exceed a hard limit if !env.IntLessThan("DB_MAX_CONNS", 10, 101) { errors = append(errors, "DB_MAX_CONNS must be less than 101") } // Must have at least one allowed origin if !env.ListIsLength("ALLOWED_ORIGINS", env.ZeroList, 0) { if env.ListLength("ALLOWED_ORIGINS", env.ZeroList) == 0 { errors = append(errors, "ALLOWED_ORIGINS must contain at least one entry") } } // Feature map must contain required keys if !env.MapHasKeys("FEATURE_FLAGS", env.ZeroMap, "auth", "payments", "notifications") { errors = append(errors, "FEATURE_FLAGS must contain auth, payments, and notifications keys") } // DEBUG must be off in production if env.IsTrue("DEBUG") && env.String("APP_ENV", "") == "production" { errors = append(errors, "DEBUG must not be true in production") } // All gating flags must be false before going live if !env.AreFalse("MAINTENANCE_MODE", "READ_ONLY", "LOCKED") { errors = append(errors, "MAINTENANCE_MODE, READ_ONLY, and LOCKED must all be false") } if len(errors) > 0 { fmt.Fprintln(os.Stderr, "Environment validation failed:") for _, e := range errors { fmt.Fprintf(os.Stderr, " - %s\n", e) } os.Exit(1) } } func main() { validateEnvironment() fmt.Println("Environment OK — starting service") } package main import ( "fmt" "os" "github.com/andreimerlescu/goenv/env" ) func validateEnvironment() { errors := []string{} // Port must be in user range if !env.IntInRange("PORT", 8080, 1024, 49151) { errors = append(errors, "PORT must be between 1024 and 49151") } // Max connections must not exceed a hard limit if !env.IntLessThan("DB_MAX_CONNS", 10, 101) { errors = append(errors, "DB_MAX_CONNS must be less than 101") } // Must have at least one allowed origin if !env.ListIsLength("ALLOWED_ORIGINS", env.ZeroList, 0) { if env.ListLength("ALLOWED_ORIGINS", env.ZeroList) == 0 { errors = append(errors, "ALLOWED_ORIGINS must contain at least one entry") } } // Feature map must contain required keys if !env.MapHasKeys("FEATURE_FLAGS", env.ZeroMap, "auth", "payments", "notifications") { errors = append(errors, "FEATURE_FLAGS must contain auth, payments, and notifications keys") } // DEBUG must be off in production if env.IsTrue("DEBUG") && env.String("APP_ENV", "") == "production" { errors = append(errors, "DEBUG must not be true in production") } // All gating flags must be false before going live if !env.AreFalse("MAINTENANCE_MODE", "READ_ONLY", "LOCKED") { errors = append(errors, "MAINTENANCE_MODE, READ_ONLY, and LOCKED must all be false") } if len(errors) > 0 { fmt.Fprintln(os.Stderr, "Environment validation failed:") for _, e := range errors { fmt.Fprintf(os.Stderr, " - %s\n", e) } os.Exit(1) } } func main() { validateEnvironment() fmt.Println("Environment OK — starting service") } package main import ( "fmt" "os" "github.com/andreimerlescu/goenv/env" ) func validateEnvironment() { errors := []string{} // Port must be in user range if !env.IntInRange("PORT", 8080, 1024, 49151) { errors = append(errors, "PORT must be between 1024 and 49151") } // Max connections must not exceed a hard limit if !env.IntLessThan("DB_MAX_CONNS", 10, 101) { errors = append(errors, "DB_MAX_CONNS must be less than 101") } // Must have at least one allowed origin if !env.ListIsLength("ALLOWED_ORIGINS", env.ZeroList, 0) { if env.ListLength("ALLOWED_ORIGINS", env.ZeroList) == 0 { errors = append(errors, "ALLOWED_ORIGINS must contain at least one entry") } } // Feature map must contain required keys if !env.MapHasKeys("FEATURE_FLAGS", env.ZeroMap, "auth", "payments", "notifications") { errors = append(errors, "FEATURE_FLAGS must contain auth, payments, and notifications keys") } // DEBUG must be off in production if env.IsTrue("DEBUG") && env.String("APP_ENV", "") == "production" { errors = append(errors, "DEBUG must not be true in production") } // All gating flags must be false before going live if !env.AreFalse("MAINTENANCE_MODE", "READ_ONLY", "LOCKED") { errors = append(errors, "MAINTENANCE_MODE, READ_ONLY, and LOCKED must all be false") } if len(errors) > 0 { fmt.Fprintln(os.Stderr, "Environment validation failed:") for _, e := range errors { fmt.Fprintf(os.Stderr, " - %s\n", e) } os.Exit(1) } } func main() { validateEnvironment() fmt.Println("Environment OK — starting service") } package main import ( "fmt" "github.com/andreimerlescu/goenv/env" ) func main() { // Your upstream system exports pipe-separated lists // SERVERS="web1|web2|web3" env.ListSeparator = "|" servers := env.List("SERVERS", env.ZeroList) for i, s := range servers { fmt.Printf("server[%d] = %s\n", i, s) } // Your config map uses pipe separation and tilde as key~value delimiter // ROUTES="home~/,admin~/admin,api~/v1" env.MapSeparator = "|" env.MapItemSeparator = "~" env.MapSplitN = 2 routes := env.Map("ROUTES", env.ZeroMap) for path, target := range routes { fmt.Printf("route %s -> %s\n", path, target) } } package main import ( "fmt" "github.com/andreimerlescu/goenv/env" ) func main() { // Your upstream system exports pipe-separated lists // SERVERS="web1|web2|web3" env.ListSeparator = "|" servers := env.List("SERVERS", env.ZeroList) for i, s := range servers { fmt.Printf("server[%d] = %s\n", i, s) } // Your config map uses pipe separation and tilde as key~value delimiter // ROUTES="home~/,admin~/admin,api~/v1" env.MapSeparator = "|" env.MapItemSeparator = "~" env.MapSplitN = 2 routes := env.Map("ROUTES", env.ZeroMap) for path, target := range routes { fmt.Printf("route %s -> %s\n", path, target) } } package main import ( "fmt" "github.com/andreimerlescu/goenv/env" ) func main() { // Your upstream system exports pipe-separated lists // SERVERS="web1|web2|web3" env.ListSeparator = "|" servers := env.List("SERVERS", env.ZeroList) for i, s := range servers { fmt.Printf("server[%d] = %s\n", i, s) } // Your config map uses pipe separation and tilde as key~value delimiter // ROUTES="home~/,admin~/admin,api~/v1" env.MapSeparator = "|" env.MapItemSeparator = "~" env.MapSplitN = 2 routes := env.Map("ROUTES", env.ZeroMap) for path, target := range routes { fmt.Printf("route %s -> %s\n", path, target) } } export AM_GO_ENV_LIST_SEPARATOR="|" export AM_GO_ENV_MAP_SEPARATOR="|" export AM_GO_ENV_MAP_ITEM_SEPARATOR="~" export AM_GO_ENV_MAP_SPLIT_N=2 export AM_GO_ENV_LIST_SEPARATOR="|" export AM_GO_ENV_MAP_SEPARATOR="|" export AM_GO_ENV_MAP_ITEM_SEPARATOR="~" export AM_GO_ENV_MAP_SPLIT_N=2 export AM_GO_ENV_LIST_SEPARATOR="|" export AM_GO_ENV_MAP_SEPARATOR="|" export AM_GO_ENV_MAP_ITEM_SEPARATOR="~" export AM_GO_ENV_MAP_SPLIT_N=2 package main import ( "fmt" "path/filepath" "github.com/andreimerlescu/goenv/env" ) func main() { u := env.User() // Derive and set a path based on the current user's home directory cacheDir := filepath.Join(u.HomeDir, ".cache", "myapp") if !env.WasSet("CACHE_DIR", cacheDir) { panic("failed to configure CACHE_DIR") } // Set with error handling if err := env.Set("RUNTIME_USER", u.Username); err != nil { panic(err) } // Remove a value that should not be visible to subprocesses if !env.WasUnset("CI_JOB_TOKEN") { fmt.Println("warning: could not unset CI_JOB_TOKEN") } // Verify cleanup if env.Exists("CI_JOB_TOKEN") { panic("CI_JOB_TOKEN still visible — aborting") } fmt.Println("CACHE_DIR:", env.String("CACHE_DIR", "")) fmt.Println("RUNTIME_USER:", env.String("RUNTIME_USER", "")) } package main import ( "fmt" "path/filepath" "github.com/andreimerlescu/goenv/env" ) func main() { u := env.User() // Derive and set a path based on the current user's home directory cacheDir := filepath.Join(u.HomeDir, ".cache", "myapp") if !env.WasSet("CACHE_DIR", cacheDir) { panic("failed to configure CACHE_DIR") } // Set with error handling if err := env.Set("RUNTIME_USER", u.Username); err != nil { panic(err) } // Remove a value that should not be visible to subprocesses if !env.WasUnset("CI_JOB_TOKEN") { fmt.Println("warning: could not unset CI_JOB_TOKEN") } // Verify cleanup if env.Exists("CI_JOB_TOKEN") { panic("CI_JOB_TOKEN still visible — aborting") } fmt.Println("CACHE_DIR:", env.String("CACHE_DIR", "")) fmt.Println("RUNTIME_USER:", env.String("RUNTIME_USER", "")) } package main import ( "fmt" "path/filepath" "github.com/andreimerlescu/goenv/env" ) func main() { u := env.User() // Derive and set a path based on the current user's home directory cacheDir := filepath.Join(u.HomeDir, ".cache", "myapp") if !env.WasSet("CACHE_DIR", cacheDir) { panic("failed to configure CACHE_DIR") } // Set with error handling if err := env.Set("RUNTIME_USER", u.Username); err != nil { panic(err) } // Remove a value that should not be visible to subprocesses if !env.WasUnset("CI_JOB_TOKEN") { fmt.Println("warning: could not unset CI_JOB_TOKEN") } // Verify cleanup if env.Exists("CI_JOB_TOKEN") { panic("CI_JOB_TOKEN still visible — aborting") } fmt.Println("CACHE_DIR:", env.String("CACHE_DIR", "")) fmt.Println("RUNTIME_USER:", env.String("RUNTIME_USER", "")) } package main import ( "log" "github.com/andreimerlescu/goenv/env" ) func init() { // Disable automatic AM_GO_ENV_* discovery env.UseMagic = false // Configure explicitly for this service's requirements env.AllowPanic = false // never panic — return fallbacks instead env.PrintErrors = true // write parse errors to stderr env.UseLogger = true // use structured INFO/ERR logger env.EnableVerboseLogging = false // no debug noise in production env.PanicNoUser = false // return default user on error env.ListSeparator = "," env.MapSeparator = "," env.MapItemSeparator = "=" env.MapSplitN = 2 // Use base-10 int64 with 64-bit size (the defaults, made explicit) env.Int64Base = 10 env.Int64BitSize = 64 log.Println("env package configured") } func main() { port := env.Int("PORT", 8080) log.Printf("port: %d", port) } package main import ( "log" "github.com/andreimerlescu/goenv/env" ) func init() { // Disable automatic AM_GO_ENV_* discovery env.UseMagic = false // Configure explicitly for this service's requirements env.AllowPanic = false // never panic — return fallbacks instead env.PrintErrors = true // write parse errors to stderr env.UseLogger = true // use structured INFO/ERR logger env.EnableVerboseLogging = false // no debug noise in production env.PanicNoUser = false // return default user on error env.ListSeparator = "," env.MapSeparator = "," env.MapItemSeparator = "=" env.MapSplitN = 2 // Use base-10 int64 with 64-bit size (the defaults, made explicit) env.Int64Base = 10 env.Int64BitSize = 64 log.Println("env package configured") } func main() { port := env.Int("PORT", 8080) log.Printf("port: %d", port) } package main import ( "log" "github.com/andreimerlescu/goenv/env" ) func init() { // Disable automatic AM_GO_ENV_* discovery env.UseMagic = false // Configure explicitly for this service's requirements env.AllowPanic = false // never panic — return fallbacks instead env.PrintErrors = true // write parse errors to stderr env.UseLogger = true // use structured INFO/ERR logger env.EnableVerboseLogging = false // no debug noise in production env.PanicNoUser = false // return default user on error env.ListSeparator = "," env.MapSeparator = "," env.MapItemSeparator = "=" env.MapSplitN = 2 // Use base-10 int64 with 64-bit size (the defaults, made explicit) env.Int64Base = 10 env.Int64BitSize = 64 log.Println("env package configured") } func main() { port := env.Int("PORT", 8080) log.Printf("port: %d", port) } export AM_GO_ENV_ALWAYS_ALLOW_PANIC=true export AM_GO_ENV_ALWAYS_PRINT_ERRORS=true export AM_GO_ENV_ALWAYS_USE_LOGGER=true export AM_GO_ENV_ENABLE_VERBOSE_LOGGING=true go run main.go export AM_GO_ENV_ALWAYS_ALLOW_PANIC=true export AM_GO_ENV_ALWAYS_PRINT_ERRORS=true export AM_GO_ENV_ALWAYS_USE_LOGGER=true export AM_GO_ENV_ENABLE_VERBOSE_LOGGING=true go run main.go export AM_GO_ENV_ALWAYS_ALLOW_PANIC=true export AM_GO_ENV_ALWAYS_PRINT_ERRORS=true export AM_GO_ENV_ALWAYS_USE_LOGGER=true export AM_GO_ENV_ENABLE_VERBOSE_LOGGING=true go run main.go env.String("KEY", "fallback") env.Int("KEY", 0) env.Int64("KEY", int64(0)) env.Float32("KEY", float32(0.0)) env.Float64("KEY", float64(0.0)) env.Bool("KEY", false) env.Duration("KEY", 5*time.Second) env.UnitDuration("KEY", 10, time.Second) // KEY=10 → 10s env.List("KEY", env.ZeroList) // "a,b,c" → []string env.Map("KEY", env.ZeroMap) // "k=v,k2=v2" → map[string]string env.String("KEY", "fallback") env.Int("KEY", 0) env.Int64("KEY", int64(0)) env.Float32("KEY", float32(0.0)) env.Float64("KEY", float64(0.0)) env.Bool("KEY", false) env.Duration("KEY", 5*time.Second) env.UnitDuration("KEY", 10, time.Second) // KEY=10 → 10s env.List("KEY", env.ZeroList) // "a,b,c" → []string env.Map("KEY", env.ZeroMap) // "k=v,k2=v2" → map[string]string env.String("KEY", "fallback") env.Int("KEY", 0) env.Int64("KEY", int64(0)) env.Float32("KEY", float32(0.0)) env.Float64("KEY", float64(0.0)) env.Bool("KEY", false) env.Duration("KEY", 5*time.Second) env.UnitDuration("KEY", 10, time.Second) // KEY=10 → 10s env.List("KEY", env.ZeroList) // "a,b,c" → []string env.Map("KEY", env.ZeroMap) // "k=v,k2=v2" → map[string]string env.Exists("KEY") // bool env.MustExist("KEY") // exits/panics if absent env.IsTrue("KEY") // true if value is "true"/"1"/"t" env.IsFalse("KEY") // true if value is "false"/"0"/"f" or unset env.AreTrue("KEY1", "KEY2", "KEY3") // true if all are true env.AreFalse("KEY1", "KEY2", "KEY3") // true if all are false/unset env.Exists("KEY") // bool env.MustExist("KEY") // exits/panics if absent env.IsTrue("KEY") // true if value is "true"/"1"/"t" env.IsFalse("KEY") // true if value is "false"/"0"/"f" or unset env.AreTrue("KEY1", "KEY2", "KEY3") // true if all are true env.AreFalse("KEY1", "KEY2", "KEY3") // true if all are false/unset env.Exists("KEY") // bool env.MustExist("KEY") // exits/panics if absent env.IsTrue("KEY") // true if value is "true"/"1"/"t" env.IsFalse("KEY") // true if value is "false"/"0"/"f" or unset env.AreTrue("KEY1", "KEY2", "KEY3") // true if all are true env.AreFalse("KEY1", "KEY2", "KEY3") // true if all are false/unset env.Set("KEY", "value") // error env.Unset("KEY") // error env.WasSet("KEY", "value") // bool — sets and verifies env.WasUnset("KEY") // bool — unsets and confirms env.Set("KEY", "value") // error env.Unset("KEY") // error env.WasSet("KEY", "value") // bool — sets and verifies env.WasUnset("KEY") // bool — unsets and confirms env.Set("KEY", "value") // error env.Unset("KEY") // error env.WasSet("KEY", "value") // bool — sets and verifies env.WasUnset("KEY") // bool — unsets and confirms env.IntLessThan("KEY", fallback, max) env.IntGreaterThan("KEY", fallback, min) env.IntInRange("KEY", fallback, min, max) env.Int64LessThan("KEY", fallback, max) env.Int64GreaterThan("KEY", fallback, min) env.Int64InRange("KEY", fallback, min, max) env.IntLessThan("KEY", fallback, max) env.IntGreaterThan("KEY", fallback, min) env.IntInRange("KEY", fallback, min, max) env.Int64LessThan("KEY", fallback, max) env.Int64GreaterThan("KEY", fallback, min) env.Int64InRange("KEY", fallback, min, max) env.IntLessThan("KEY", fallback, max) env.IntGreaterThan("KEY", fallback, min) env.IntInRange("KEY", fallback, min, max) env.Int64LessThan("KEY", fallback, max) env.Int64GreaterThan("KEY", fallback, min) env.Int64InRange("KEY", fallback, min, max) env.ListLength("KEY", fallback) env.ListIsLength("KEY", fallback, wantLength) env.ListContains("KEY", fallback, "needle") env.MapHasKey("KEY", fallback, "key") env.MapHasKeys("KEY", fallback, "k1", "k2", "k3") env.ListLength("KEY", fallback) env.ListIsLength("KEY", fallback, wantLength) env.ListContains("KEY", fallback, "needle") env.MapHasKey("KEY", fallback, "key") env.MapHasKeys("KEY", fallback, "k1", "k2", "k3") env.ListLength("KEY", fallback) env.ListIsLength("KEY", fallback, wantLength) env.ListContains("KEY", fallback, "needle") env.MapHasKey("KEY", fallback, "key") env.MapHasKeys("KEY", fallback, "k1", "k2", "k3") env.User() // *user.User — current OS user with safe fallback env.User() // *user.User — current OS user with safe fallback env.User() // *user.User — current OS user with safe fallback