SuperProdfor Kubenetes
Guide

Advanced Usage

Prefixes, required fields, and custom processing

master

Overview

This page covers the advanced features of envconfig that give you precise control over how environment variables are mapped to your Go structs. You'll learn how to use struct tags to set prefixes, mark fields as required, supply default values, and implement custom decoders for types that envconfig doesn't handle out of the box. Mastering these features lets you build robust, self-documenting configuration layers without writing repetitive parsing code.


Prerequisites

Before working through this guide, you should have:

  • Go installed (any version that supports modules)
  • The envconfig library added to your module (go get github.com/kelseyhightower/envconfig)
  • Familiarity with Go structs and struct tags
  • A working understanding of the basic envconfig workflow (defining a struct, calling envconfig.Process)

If you haven't completed the basic setup yet, start with the Getting Started guide first.


Installation

If you haven't added envconfig to your project yet, run:

Step 1 — Add the dependency

go get github.com/kelseyhightower/envconfig

Step 2 — Import the package in your code

import "github.com/kelseyhightower/envconfig"

No further installation steps are required. All advanced features described on this page are part of the same package.


Configuration

Envconfig's behavior is controlled entirely through struct tags on your configuration fields. There are no global settings or separate config files.

TagValueEffect
envconfig"MY_VAR_NAME"Overrides the default environment variable name for this field. Envconfig first looks for PREFIX_MY_VAR_NAME; if not found, it falls back to the exact tag value.
defaultAny stringUsed as the field's value when the environment variable is absent. The default is parsed through the same type conversion as a live value.
required"true"Causes envconfig.Process to return an error if the environment variable is absent. A present-but-empty value does not trigger an error.
ignored"true"Tells envconfig to skip this field entirely, even if a matching environment variable exists.
split_words"true"Converts a CamelCase field name into a SCREAMING_SNAKE_CASE environment variable name. For example, AutoSplitVar becomes MYAPP_AUTO_SPLIT_VAR instead of MYAPP_AUTOSPLITVAR.

Tag precedence for variable name resolution

When resolving which environment variable to read, envconfig uses this order:

  1. PREFIX_ + the value of the envconfig tag (e.g., MYAPP_MANUAL_OVERRIDE_1)
  2. The bare value of the envconfig tag without a prefix (e.g., SERVICE_HOST) — useful for cross-service variables
  3. PREFIX_ + the transformed field name (applying split_words if set)

This fallback behavior means you can reference shared infrastructure variables (like SERVICE_HOST) directly without needing a per-app prefix.


Usage

Using a prefix

Pass your application's prefix as the first argument to envconfig.Process. Envconfig prepends it (uppercased) to every field name when constructing the expected environment variable name.

err := envconfig.Process("myapp", &cfg)

With this call, a field named Port maps to MYAPP_PORT.


Overriding the variable name

Use the envconfig tag when the default naming convention doesn't match your environment or when you need to share a variable across applications.

type Specification struct {
    ServiceHost string `envconfig:"SERVICE_HOST"`
    Debug       bool
}
export SERVICE_HOST=127.0.0.1
export MYAPP_DEBUG=true

Envconfig looks for MYAPP_SERVICE_HOST first. If that's not set, it falls back to SERVICE_HOST directly — so both naming styles work.


Requiring a field

Mark a field required:"true" to make envconfig.Process return an error when the variable is missing. This is the right choice for credentials, service endpoints, or any value your application cannot safely default.

type Specification struct {
    DatabaseURL string `required:"true"`
    APIKey      string `required:"true" split_words:"true"`
}
# If MYAPP_DATABASEURL is unset, Process returns an error.
export MYAPP_DATABASE_URL=postgres://localhost/mydb
export MYAPP_API_KEY=secret

Note: Setting a required variable to an empty string (export MYAPP_DATABASEURL="") does not trigger the error. Use application-level validation if you also want to reject empty values.


Setting defaults

Use default for optional configuration that has a sensible fallback. The default value goes through the same parsing as a live environment variable, so you can use duration strings, comma-separated slices, and so on.

type Specification struct {
    Port    int           `default:"8080"`
    Timeout time.Duration `default:"30s"`
    LogLevel string       `default:"info"`
}

Splitting CamelCase field names

Add split_words:"true" to make envconfig insert underscores at word boundaries in CamelCase names. This is the most readable option when your field names are multi-word.

type Specification struct {
    // Without split_words: looks for MYAPP_AUTOSPLITVAR
    // With split_words:    looks for MYAPP_AUTO_SPLIT_VAR
    AutoSplitVar string `split_words:"true"`
}

Tip: Numbers are grouped with the preceding word. If the automatic splitting doesn't produce the name you want, use a manual envconfig tag override instead.


Ignoring fields

Apply ignored:"true" to struct fields that should never be populated from the environment — for example, fields that are computed at runtime or injected by another mechanism.

type Specification struct {
    ComputedAt time.Time `ignored:"true"`
    Port       int
}

Implementing a custom decoder

For types that envconfig can't parse natively, implement the envconfig.Decoder interface on your type:

type Decode(value string) error

Envconfig calls this method (on a pointer receiver) instead of its built-in parsing. You can also implement flag.Value's Set(string) error method as an alternative — envconfig recognises both interfaces.

See the Examples section for complete custom decoder implementations.


Examples

Example 1 — Combining required, default, and split_words

This struct shows all three annotation types working together.

export MYAPP_MANUAL_OVERRIDE_1="overridden value"
export MYAPP_REQUIRED_VAR="must-have"
# MYAPP_DEFAULT_VAR is intentionally unset — the default kicks in
# MYAPP_AUTO_SPLIT_VAR is intentionally unset — also uses its default
package main

import (
    "fmt"
    "log"

    "github.com/kelseyhightower/envconfig"
)

type Specification struct {
    ManualOverride1         string `envconfig:"manual_override_1"`
    DefaultVar              string `default:"foobar"`
    RequiredVar             string `required:"true"`
    IgnoredVar              string `ignored:"true"`
    AutoSplitVar            string `split_words:"true" default:"auto-default"`
    RequiredAndAutoSplitVar string `required:"true" split_words:"true"`
}

func main() {
    var s Specification
    err := envconfig.Process("myapp", &s)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ManualOverride1:         %s\n", s.ManualOverride1)
    fmt.Printf("DefaultVar:              %s\n", s.DefaultVar)
    fmt.Printf("RequiredVar:             %s\n", s.RequiredVar)
    fmt.Printf("IgnoredVar:              %s\n", s.IgnoredVar)
    fmt.Printf("AutoSplitVar:            %s\n", s.AutoSplitVar)
}

Expected output:

ManualOverride1:         overridden value
DefaultVar:              foobar
RequiredVar:             must-have
IgnoredVar:              
AutoSplitVar:            auto-default

Example 2 — Custom decoder for net.IP

Use envconfig.Decoder to handle a type that envconfig doesn't natively support.

export DNS_SERVER=8.8.8.8
package main

import (
    "fmt"
    "log"
    "net"

    "github.com/kelseyhightower/envconfig"
)

type IPDecoder net.IP

func (ipd *IPDecoder) Decode(value string) error {
    *ipd = IPDecoder(net.ParseIP(value))
    return nil
}

type DNSConfig struct {
    Address IPDecoder `envconfig:"DNS_SERVER"`
}

func main() {
    var cfg DNSConfig
    err := envconfig.Process("", &cfg)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("DNS address: %s\n", net.IP(cfg.Address))
}

Expected output:

DNS address: 8.8.8.8

Example 3 — Custom decoder for a complex map type

When your environment variable encodes structured data (here, a semicolon-delimited list of JSON arrays), a custom decoder keeps the struct clean.

export SMS_PROVIDER_WITH_WEIGHT='IND=[{"name":"SMSProvider1","weight":70},{"name":"SMSProvider2","weight":30}];US=[{"name":"SMSProvider1","weight":100}]'
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "strings"

    "github.com/kelseyhightower/envconfig"
)

type providerDetails struct {
    Name   string
    Weight int
}

type SMSProviderDecoder map[string][]providerDetails

func (sd *SMSProviderDecoder) Decode(value string) error {
    result := map[string][]providerDetails{}
    for _, pair := range strings.Split(value, ";") {
        kv := strings.SplitN(pair, "=", 2)
        if len(kv) != 2 {
            return fmt.Errorf("invalid map item: %q", pair)
        }
        var providers []providerDetails
        if err := json.Unmarshal([]byte(kv[1]), &providers); err != nil {
            return fmt.Errorf("invalid map json: %w", err)
        }
        result[kv[0]] = providers
    }
    *sd = SMSProviderDecoder(result)
    return nil
}

type SMSProviderConfig struct {
    ProviderWithWeight SMSProviderDecoder `envconfig:"SMS_PROVIDER_WITH_WEIGHT"`
}

func main() {
    var cfg SMSProviderConfig
    if err := envconfig.Process("", &cfg); err != nil {
        log.Fatal(err)
    }
    for region, providers := range cfg.ProviderWithWeight {
        fmt.Printf("%s:\n", region)
        for _, p := range providers {
            fmt.Printf("  %s (weight %d)\n", p.Name, p.Weight)
        }
    }
}

Expected output:

IND:
  SMSProvider1 (weight 70)
  SMSProvider2 (weight 30)
US:
  SMSProvider1 (weight 100)

Troubleshooting

envconfig.Process returns an error for a required field I've set

Symptom: You see an error like required key MYAPP_MYFIELD missing value even though you exported the variable.

Likely cause: The environment variable name doesn't match what envconfig expects. Common mismatches:

  • You used split_words:"true" but exported the non-split name (e.g., exported MYAPP_MYFIELD instead of MYAPP_MY_FIELD).
  • You have a manual envconfig tag but the casing doesn't match.
  • The variable was exported in a different shell session.

Fix: Print the expected variable name with envconfig.Usage("myprefix", &cfg) (see godoc), or temporarily add fmt.Println(os.Getenv("MYAPP_MY_FIELD")) to confirm the value is visible at runtime.


A field always has its zero value even though the environment variable is set

Symptom: The struct field is empty/zero after Process, but echo $MYAPP_MYVAR shows a value.

Likely cause:

  • The field has ignored:"true" set.
  • The field name or prefix doesn't produce the variable name you think it does (see CamelCase/split_words rules above).
  • The variable is set in the environment but envconfig.Process is called before the variable is visible (e.g., in a test that sets os.Setenv after the call).

Fix: Double-check the tag and the exact variable name envconfig is looking for. Remove ignored:"true" if it was added accidentally. In tests, set os.Setenv before calling Process.


My custom Decode method is never called

Symptom: You implemented Decode(string) error but envconfig uses its built-in parsing instead.

Likely cause: The method is defined on a value receiver instead of a pointer receiver, or the type itself (not a pointer to it) is used as the struct field type when it needs to be a pointer.

Fix: Ensure the signature is func (t *YourType) Decode(value string) error with a pointer receiver. Envconfig checks for the interface on both the type and its pointer; however, pointer receivers require an addressable value, which is satisfied when the field is part of a struct passed by pointer to Process.


A field with default is still empty after Process

Symptom: The default value is not applied even though the environment variable is not set.

Likely cause: The environment variable is set to an empty string (export MYAPP_FIELD=""). Envconfig treats a present-but-empty variable as a valid (empty) value and does not fall back to the default.

Fix: Either unset the variable entirely (unset MYAPP_FIELD) or handle the empty-string case in your application logic after Process returns.


The fallback to a bare tag name (without prefix) isn't working

Symptom: You have envconfig:"SERVICE_HOST" and exported SERVICE_HOST, but the field is empty when using a prefix.

Likely cause: Envconfig first looks for PREFIX_SERVICE_HOST. If that variable is set (even to empty), it uses that value and does not fall back.

Fix: Make sure PREFIX_SERVICE_HOST is not set in your environment. The fallback to the bare tag value only happens when the prefixed name is completely absent.