go-envconfig

repository·main·Indexed 22 days ago

https://github.com/sethvargo/go-envconfig

A Go library that populates struct fields from environment variables or custom lookup functions. It supports prefixes, default values, custom decoders, and complex types including slices, maps, and Base64/Hex-encoded bytes. The library provides flexible configuration via struct tags and supports custom Mutators for value transformation and various Lookuper implementations for environment simulation and testing.

Tokens
3K
Snippets
12
Records
18
Agent score
78%

What's inside go-envconfig

  1. Decoding complex types

    main

    Envconfig supports several ways to decode environment variable strings into complex Go types:

    • Durations: time.Duration values are parsed as standard Go duration strings (e.g., "10m").
    • Interfaces: Types implementing TextUnmarshaler, BinaryUnmarshaler, json.Unmarshaler, or gob.Decoder are processed using those respective methods.
    • Slices: Decoded from comma-separated values (e.g., "a,b,c"). Byte slices are treated as strings.
    • Maps: Decoded from comma-separated key:value pairs (e.g., "a:b,c:d"). You can customize the separator using the separator tag.
    • Custom Decoders: Implement the EnvDecode(ctx context.Context, val string) error method on your type to define custom logic.
    type MyCustomType struct {
      value string
    }
    
    func (t *MyCustomType) EnvDecode(ctx context.Context, val string) error {
      resolved := someComplexFunction(val)
      t.value = resolved
      return nil
    }
  2. How overwrite and default interact

    main

    The overwrite tag modifies how default values are applied based on the initial state of the struct field:

    1. If the field has a zero value:

      • No env var: Field gets the default value.
      • Env var present: Field gets the env var value.
    2. If the field has a non-zero value:

      • No env var: Field keeps its existing value (the default is ignored).
      • Env var present: Field is overwritten with the env var value.
  3. Basic usage of envconfig

    main

    To populate a struct from environment variables, define a struct with fields using the env tag and call envconfig.Process(ctx, &target). All fields intended for processing must be public. Nested structs are supported, and if a nested struct is a pointer, it will be automatically instantiated.

    type MyConfig struct {
      Port     string `env:"PORT"`
      Username string `env:"USERNAME"`
    }
    
    // ... set environment variables PORT=5555 and USERNAME=yoyo ...
    
    func main() {
      ctx := context.Background()
      var c MyConfig
      if err := envconfig.Process(ctx, &c); err != nil {
        log.Fatal(err)
      }
    }
  4. Test envconfig using MapLookuper

    main

    To avoid relying on global environment variables during testing (which prevents parallel testing), use envconfig.ProcessWith with a MapLookuper. This allows you to provide a static map of key-value pairs to simulate the environment.

    lookuper := envconfig.MapLookuper(map[string]string{
      "FOO": "bar",
      "ZIP": "zap",
    })
    
    var config Config
    err := envconfig.ProcessWith(ctx, &envconfig.Config{
      Target:   &config,
      Lookuper: lookuper,
    })
  5. Configure struct tags with envconfig

    main

    Use the env struct tag to control how fields are processed. Supported options include:

    • required: Errors if the environment variable is unset.
    • default=<value>: Sets a default if the variable is unset. You can use values from other environment variables (e.g., default=$OTHER_VAR). To use a literal $, use a double backslash (e.g., default=\$5.00).
    • prefix=<string>: Adds a prefix to the environment variable keys for child structs or fields. This is useful for grouping configurations (e.g., prefix=CACHE_ makes a field HOST look for CACHE_HOST).
    • overwrite: Forces overwriting existing non-zero struct values if the environment variable is provided.
    • delimiter=<char>: Custom character for slice/map entries (default is ,).
    • separator=<char>: Custom character for map key/value separation (default is :).
    • noinit: Prevents automatic initialization of fields unless the environment variable is provided.
    • decodeunset: Forces decoders to run even if the environment variable is unset.
    type MyStruct struct {
      Port     string `env:"PORT, required"` 
      User     string `env:"USER, default=$CURRENT_USER"` 
      Amount   string `env:"AMOUNT, default=\\$5.00"` 
      MyVar    []string `env:"MYVAR, delimiter=;"` 
      MyMap    map[string]string `env:"MYVAR, separator=|"` 
      Cache    *RedisConfig `env:", prefix=CACHE_"` 
    }
  6. Implement a custom Mutator using the Mutator interface

    main

    A Mutator acts like middleware for environment variable processing. It allows you to intercept and alter the raw environment variable value before it is converted into the target struct field type. This is useful for transformations like stripping prefixes, decrypting values, or reformatting strings.

    When implementing EnvMutate, you receive:

    • originalKey: The unmodified environment variable name from the struct.
    • resolvedKey: The fully-resolved name (including any prefixes applied during processing).
    • originalValue: The raw value from the environment before any mutations.
    • currentValue: The value as it exists after any previous mutators in the stack have run.

    The method returns:

    • newValue: The value to be passed to the next mutator or the final decoder.
    • stop: A boolean. If true, subsequent mutators in the stack are skipped.
    • err: Any error encountered during mutation.
    type Mutator interface {
    	EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error)
    }
  7. Use MustProcess for quick initializations

    main

    MustProcess is a helper that calls Process and panics if an error occurs. It returns the populated struct, making it useful for anonymous initializations in scripts or CLIs where graceful error handling is not a priority.

    Note: This is not recommended for production services due to the panic behavior.

    var env = envconfig.MustProcess(context.Background(), &struct{
      Field string `env:"FIELD,required"`
    })
  8. Resolve values using different Lookupers

    main

    A Lookuper provides the mechanism for finding environment variable values. You can use different implementations depending on your needs:

    • OsLookuper(): Uses os.LookupEnv (default).
    • MapLookuper(map[string]string): Uses a provided map. Ideal for testing to avoid mutating the global environment.
    • PrefixLookuper(prefix string, l Lookuper): Prepends a prefix to all keys looked up by the underlying lookuper.
    • MultiLookuper(lookupers ...Lookuper): Searches through a list of lookupers in the order provided.
  9. Configure decoding with Config and ProcessWith

    main

    For advanced control, use ProcessWith by providing a Config struct. This allows you to customize the Lookuper, default delimiters, separators, and other behaviors.

    Process can also accept a *Config as its target to use that configuration for the decoding process.

  10. Populate structs from environment variables with Process

    main

    Use Process to decode environment variables into a struct. Struct fields must be tagged with env:"KEY" to specify the environment variable name. The key is case-sensitive.

    Supported types include all built-in types except Func and Chan. If a field is a pointer, envconfig will automatically initialize it.

    type MyStruct struct {
      A string `env:"A"` // resolves A to $A
      B string `env:"B,required"` // resolves B to $B, errors if $B is unset
      C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
    }
    
    var cfg MyStruct
    err := envconfig.Process(ctx, &cfg)
  11. Migrate from LegacyMutatorFunc

    main

    The LegacyMutatorFunc helper is deprecated. It was used to wrap older mutator functions that only provided the key and value. While it can still be used to ease transitions, it is inherently lossy because it cannot access the resolvedKey or originalValue provided by the new Mutator interface.

    Recommendation: Use MutatorFunc instead to take full advantage of the available context.

    // Deprecated: Use [MutatorFunc] instead.
    func LegacyMutatorFunc(fn func(ctx context.Context, key, value string) (string, error)) MutatorFunc