goenvsubst

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2025 License: MIT Imports: 3 Imported by: 0

README

goenvsubst

Go Reference Go Report Card GitHub Actions Workflow Status Codacy Badge Codacy Badge

A Go package for recursively replacing environment variable references in Go data structures with their actual values from the environment.

Features

  • Comprehensive Type Support: Works with structs, slices, maps, arrays, and pointers
  • Nested Structures: Handles deeply nested and complex data structures
  • Safe Pointer Handling: Safely processes nil pointers without panics
  • In-Place Modification: Modifies data structures in-place for efficiency
  • Environment Variable Format: Uses $VAR_NAME format for variable references
  • Missing Variable Handling: Replaces undefined or empty variables with empty strings
  • Zero Dependencies: Pure Go implementation with no external dependencies

Installation

go get github.com/iamolegga/goenvsubst

Quick Start

package main

import (
    "fmt"
    "os"
    "github.com/iamolegga/goenvsubst"
)

func main() {
    // Set environment variable
    os.Setenv("DATABASE_URL", "postgres://localhost:5432/mydb")
    
    config := &struct {
        DatabaseURL string
        Debug       bool
    }{
        DatabaseURL: "$DATABASE_URL",
        Debug:       true,
    }
    
    err := goenvsubst.Do(config)
    if err != nil {
        panic(err)
    }
    
    fmt.Printf("Database URL: %s\n", config.DatabaseURL)
    // Output: Database URL: postgres://localhost:5432/mydb
}

Usage Examples

Struct Fields
type Config struct {
    Host     string
    Port     string
    Username string
    Password string
}

config := &Config{
    Host:     "$DB_HOST",
    Port:     "$DB_PORT", 
    Username: "$DB_USER",
    Password: "$DB_PASS",
}

goenvsubst.Do(config)
Slices and Arrays
// Slice
urls := &[]string{"$API_URL", "$BACKUP_URL", "https://static.example.com"}
goenvsubst.Do(urls)

// Array
servers := &[3]string{"$SERVER1", "$SERVER2", "$SERVER3"}
goenvsubst.Do(servers)
Maps
config := &map[string]string{
    "database_url": "$DATABASE_URL",
    "redis_url":    "$REDIS_URL",
    "api_key":      "$API_KEY",
}
goenvsubst.Do(config)
// Note: Map keys are never modified, only values
Complex Nested Structures
type DatabaseConfig struct {
    URL      string
    Username string
    Password string
}

type AppConfig struct {
    Database DatabaseConfig
    Services []string
    Env      map[string]string
}

config := &AppConfig{
    Database: DatabaseConfig{
        URL:      "$DATABASE_URL",
        Username: "$DB_USER",
        Password: "$DB_PASS",
    },
    Services: []string{"$SERVICE1", "$SERVICE2"},
    Env: map[string]string{
        "LOG_LEVEL": "$LOG_LEVEL",
        "DEBUG":     "$DEBUG_MODE",
    },
}

goenvsubst.Do(config)
Pointers
// Safe with nil pointers
var config *struct{ Value string }
goenvsubst.Do(&config) // No error, no operation

// Works with actual pointers
value := "$MY_VALUE"
ptr := &value
goenvsubst.Do(&ptr)

Supported Data Types

Type Support Notes
string Environment variables are substituted
struct All string fields are processed recursively
slice All elements are processed recursively
array All elements are processed recursively
map Only values are processed, keys remain unchanged
pointer Safely handles nil pointers
int, bool, etc. Non-string types are ignored (no substitution)
interface{} Not currently supported

Environment Variable Format

Environment variables should be referenced using the $VAR_NAME format:

// Supported formats
"$DATABASE_URL"           // ✅ Full string replacement
"$MISSING_VAR"            // ✅ Undefined vars become empty strings

// Not supported formats
"${DATABASE_URL}"         // ❌ Braces not supported
"prefix-$API_KEY-suffix"  // ❌ Partial substitution not supported

Error Handling

The Do function returns an error for future extensibility, but currently is designed to be robust:

err := goenvsubst.Do(config)
if err != nil {
    log.Printf("Failed to substitute environment variables: %v", err)
    return
}

Important Notes

  • In-Place Modification: The function modifies the input data structure directly
  • Map Keys: Only map values are processed, keys are never modified
  • Missing Variables: Undefined or empty environment variables are replaced with empty strings
  • Thread Safety: Safe for concurrent use (doesn't modify global state)
  • Nil Pointers: Handled safely without causing panics
  • Type Safety: Only string values are processed for substitution

Testing

Run the tests:

go test -v

Run example tests:

go test -v -run Example

Documentation

For detailed documentation and more examples, visit: pkg.go.dev Documentation

License

MIT License - see LICENSE file for details.

Documentation

Overview

Package goenvsubst provides functionality to recursively replace environment variable references in Go data structures with their actual values from the environment.

The package supports various Go data types including structs, slices, maps, arrays, and pointers, both as top-level inputs and nested within other structures.

Environment variables should be referenced in the format $VAR_NAME. If an environment variable is not set or is empty, it will be replaced with an empty string.

Basic Usage

The main function Do() accepts any Go data structure and modifies it in-place:

import "github.com/iamolegga/goenvsubst"

// Set environment variable
os.Setenv("DATABASE_URL", "postgres://localhost:5432/mydb")

config := &struct {
	DatabaseURL string
	Debug       bool
}{
	DatabaseURL: "$DATABASE_URL",
	Debug:       true,
}

err := goenvsubst.Do(config)
if err != nil {
	log.Fatal(err)
}
// config.DatabaseURL is now "postgres://localhost:5432/mydb"

Struct Fields

Environment variable substitution works with any string field in a struct:

type Config struct {
	Host     string
	Port     string
	Username string
	Password string
}

config := &Config{
	Host:     "$DB_HOST",
	Port:     "$DB_PORT",
	Username: "$DB_USER",
	Password: "$DB_PASS",
}

goenvsubst.Do(config)

Slices and Arrays

String elements in slices and arrays are processed:

// Slice example
urls := []string{"$API_URL", "$BACKUP_URL", "https://static.example.com"}
goenvsubst.Do(&urls)

// Array example
servers := [3]string{"$SERVER1", "$SERVER2", "$SERVER3"}
goenvsubst.Do(&servers)

Maps

Map values (but not keys) are processed for environment variable substitution:

config := map[string]string{
	"database_url": "$DATABASE_URL",
	"redis_url":    "$REDIS_URL",
	"api_key":      "$API_KEY",
}
goenvsubst.Do(&config)

Nested Structures

The package handles deeply nested structures:

type DatabaseConfig struct {
	URL      string
	Username string
	Password string
}

type AppConfig struct {
	Database DatabaseConfig
	Services []string
	Env      map[string]string
}

config := &AppConfig{
	Database: DatabaseConfig{
		URL:      "$DATABASE_URL",
		Username: "$DB_USER",
		Password: "$DB_PASS",
	},
	Services: []string{"$SERVICE1", "$SERVICE2"},
	Env: map[string]string{
		"LOG_LEVEL": "$LOG_LEVEL",
		"DEBUG":     "$DEBUG_MODE",
	},
}

goenvsubst.Do(config)

Pointers

The package safely handles pointers, including nil pointers:

var config *struct {
	Value string
}

// Safe to call with nil pointer
goenvsubst.Do(&config) // No error, no operation

// With actual pointer
config = &struct{ Value string }{"$MY_VALUE"}
goenvsubst.Do(config)

Complex Example

A real-world configuration structure:

type ServerConfig struct {
	Host string `json:"host"`
	Port string `json:"port"`
}

type AppConfig struct {
	Server    ServerConfig            `json:"server"`
	Database  string                  `json:"database"`
	Redis     string                  `json:"redis"`
	Services  []string                `json:"services"`
	Features  map[string]bool         `json:"features"`
	Endpoints map[string]string       `json:"endpoints"`
	Secrets   map[string]*string      `json:"secrets"`
}

// Set environment variables
os.Setenv("APP_HOST", "0.0.0.0")
os.Setenv("APP_PORT", "8080")
os.Setenv("DATABASE_URL", "postgres://localhost/myapp")
os.Setenv("REDIS_URL", "redis://localhost:6379")
os.Setenv("API_ENDPOINT", "https://api.example.com")

config := &AppConfig{
	Server: ServerConfig{
		Host: "$APP_HOST",
		Port: "$APP_PORT",
	},
	Database: "$DATABASE_URL",
	Redis:    "$REDIS_URL",
	Services: []string{"$SERVICE_AUTH", "$SERVICE_PAYMENT"},
	Features: map[string]bool{
		"feature_a": true,
		"feature_b": false,
	},
	Endpoints: map[string]string{
		"api":     "$API_ENDPOINT",
		"webhook": "$WEBHOOK_URL",
	},
	Secrets: map[string]*string{
		"jwt_secret": func() *string { s := "$JWT_SECRET"; return &s }(),
	},
}

err := goenvsubst.Do(config)
if err != nil {
	log.Fatal(err)
}

Error Handling

The Do function returns an error if there are issues during processing. Currently, the function is designed to be robust and typically returns nil, but error handling is provided for future extensibility:

err := goenvsubst.Do(config)
if err != nil {
	log.Printf("Failed to substitute environment variables: %v", err)
	return
}

Important Notes

- Only string values are processed for environment variable substitution - Map keys are never modified, only values - Missing or empty environment variables are replaced with empty strings - The function modifies the input data structure in-place - Nil pointers are handled safely without causing panics - The function is safe for concurrent use as it doesn't modify global state

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Do

func Do(v any) error

Do recursively walks through any Go data structure (structs, slices, maps, arrays, pointers) and replaces environment variable references in string values with their actual values from the environment. Environment variables should be in the format $VAR_NAME. Supports top-level and nested: structs, slices, arrays, maps, and pointers.

Example

ExampleDo demonstrates basic usage with a struct

package main

import (
	"fmt"
	"os"

	"github.com/iamolegga/goenvsubst"
)

func main() {
	// Set up environment variables
	os.Setenv("DATABASE_URL", "postgres://localhost:5432/mydb")
	os.Setenv("API_KEY", "secret123")
	defer func() {
		os.Unsetenv("DATABASE_URL")
		os.Unsetenv("API_KEY")
	}()

	config := &struct {
		DatabaseURL string
		APIKey      string
		Debug       bool
	}{
		DatabaseURL: "$DATABASE_URL",
		APIKey:      "$API_KEY",
		Debug:       true,
	}

	err := goenvsubst.Do(config)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Database URL: %s\n", config.DatabaseURL)
	fmt.Printf("API Key: %s\n", config.APIKey)
	fmt.Printf("Debug: %t\n", config.Debug)

}
Output:
Database URL: postgres://localhost:5432/mydb
API Key: secret123
Debug: true
Example (Array)

ExampleDo_array demonstrates usage with arrays

package main

import (
	"fmt"
	"os"

	"github.com/iamolegga/goenvsubst"
)

func main() {
	// Set up environment variables
	os.Setenv("SERVER1", "server1.example.com")
	os.Setenv("SERVER2", "server2.example.com")
	defer func() {
		os.Unsetenv("SERVER1")
		os.Unsetenv("SERVER2")
	}()

	servers := [3]string{"$SERVER1", "$SERVER2", "server3.example.com"}

	err := goenvsubst.Do(&servers)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	for i, server := range servers {
		fmt.Printf("Server %d: %s\n", i+1, server)
	}

}
Output:
Server 1: server1.example.com
Server 2: server2.example.com
Server 3: server3.example.com
Example (Map)

ExampleDo_map demonstrates usage with maps

package main

import (
	"fmt"
	"os"

	"github.com/iamolegga/goenvsubst"
)

func main() {
	// Set up environment variables
	os.Setenv("REDIS_URL", "redis://localhost:6379")
	os.Setenv("MONGO_URL", "mongodb://localhost:27017")
	defer func() {
		os.Unsetenv("REDIS_URL")
		os.Unsetenv("MONGO_URL")
	}()

	config := map[string]string{
		"redis":    "$REDIS_URL",
		"mongodb":  "$MONGO_URL",
		"static":   "https://cdn.example.com",
		"$ENV_KEY": "this-key-wont-change", // Keys are not substituted
	}

	err := goenvsubst.Do(&config)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Redis: %s\n", config["redis"])
	fmt.Printf("MongoDB: %s\n", config["mongodb"])
	fmt.Printf("Static: %s\n", config["static"])

}
Output:
Redis: redis://localhost:6379
MongoDB: mongodb://localhost:27017
Static: https://cdn.example.com
Example (NestedStructure)

ExampleDo_nestedStructure demonstrates usage with complex nested structures

package main

import (
	"fmt"
	"os"

	"github.com/iamolegga/goenvsubst"
)

func main() {
	// Set up environment variables
	os.Setenv("DB_HOST", "localhost")
	os.Setenv("DB_PORT", "5432")
	os.Setenv("API_ENDPOINT", "https://api.example.com")
	defer func() {
		os.Unsetenv("DB_HOST")
		os.Unsetenv("DB_PORT")
		os.Unsetenv("API_ENDPOINT")
	}()

	type DatabaseConfig struct {
		Host string
		Port string
	}

	type AppConfig struct {
		Database  DatabaseConfig
		Services  []string
		Endpoints map[string]string
	}

	config := &AppConfig{
		Database: DatabaseConfig{
			Host: "$DB_HOST",
			Port: "$DB_PORT",
		},
		Services: []string{"$SERVICE1", "static-service"},
		Endpoints: map[string]string{
			"api":    "$API_ENDPOINT",
			"health": "/health",
		},
	}

	err := goenvsubst.Do(config)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Database Host: %s\n", config.Database.Host)
	fmt.Printf("Database Port: %s\n", config.Database.Port)
	fmt.Printf("API Endpoint: %s\n", config.Endpoints["api"])
	fmt.Printf("Health Endpoint: %s\n", config.Endpoints["health"])
	if config.Services[0] == "" {
		fmt.Printf("Service 1:\n")
	} else {
		fmt.Printf("Service 1: %s\n", config.Services[0])
	}
	fmt.Printf("Service 2: %s\n", config.Services[1])

}
Output:
Database Host: localhost
Database Port: 5432
API Endpoint: https://api.example.com
Health Endpoint: /health
Service 1:
Service 2: static-service
Example (Pointers)

ExampleDo_pointers demonstrates usage with pointers

package main

import (
	"fmt"
	"os"

	"github.com/iamolegga/goenvsubst"
)

func main() {
	// Set up environment variables
	os.Setenv("SECRET_KEY", "my-secret-key")
	defer os.Unsetenv("SECRET_KEY")

	// Pointer to string
	secretValue := "$SECRET_KEY"
	err := goenvsubst.Do(&secretValue)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Secret: %s\n", secretValue)

	// Nil pointer (safe to process)
	var nilPtr *string
	err = goenvsubst.Do(&nilPtr)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Nil pointer handled safely: %v\n", nilPtr == nil)

}
Output:
Secret: my-secret-key
Nil pointer handled safely: true
Example (Slice)

ExampleDo_slice demonstrates usage with slices

package main

import (
	"fmt"
	"os"

	"github.com/iamolegga/goenvsubst"
)

func main() {
	// Set up environment variables
	os.Setenv("SERVICE1", "auth-service")
	os.Setenv("SERVICE2", "payment-service")
	defer func() {
		os.Unsetenv("SERVICE1")
		os.Unsetenv("SERVICE2")
	}()

	services := []string{"$SERVICE1", "$SERVICE2", "static-service"}

	err := goenvsubst.Do(&services)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	for i, service := range services {
		fmt.Printf("Service %d: %s\n", i+1, service)
	}

}
Output:
Service 1: auth-service
Service 2: payment-service
Service 3: static-service

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL