Advanced Usage
Prefixes, required fields, and custom processing
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.
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.
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.
Envconfig's behavior is controlled entirely through struct tags on your configuration fields. There are no global settings or separate config files.
| Tag | Value | Effect |
|---|---|---|
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. |
default | Any string | Used 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:
PREFIX_+ the value of theenvconfigtag (e.g.,MYAPP_MANUAL_OVERRIDE_1)- The bare value of the
envconfigtag without a prefix (e.g.,SERVICE_HOST) — useful for cross-service variables PREFIX_+ the transformed field name (applyingsplit_wordsif set)
This fallback behavior means you can reference shared infrastructure variables (like SERVICE_HOST) directly without needing a per-app prefix.
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
envconfigtag 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.
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)
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., exportedMYAPP_MYFIELDinstead ofMYAPP_MY_FIELD). - You have a manual
envconfigtag 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_wordsrules above). - The variable is set in the environment but
envconfig.Processis called before the variable is visible (e.g., in a test that setsos.Setenvafter 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.