A convenient, type-safe configuration library for Go that populates your structs from multiple sources including environment variables, command-line flags, JSON files, and more.
- Multiple Providers: Load configuration from environment variables, flags, JSON, or static values
- Type Safety: Automatic type conversion for primitive types, slices, maps, and custom types
- Struct Tags: Simple tag-based configuration using
env,flag,jsontags - Composable: Chain multiple providers with fallback behavior
- Extensible: Create custom providers for any configuration source
- CLI Support: Built-in CLI framework with command hierarchies and help generation
package main
import (
"context"
"fmt"
"os"
"github.com/upfluence/cfg"
)
type Config struct {
Host string `env:"HOST" flag:"host"`
Port int `env:"PORT" flag:"port"`
Debug bool `env:"DEBUG" flag:"debug"`
Timeout time.Duration `env:"TIMEOUT" flag:"timeout"`
}
func main() {
var c Config
ctx := context.Background()
if err := cfg.NewDefaultConfigurator().Populate(ctx, &c); err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err.Error())
os.Exit(1)
}
fmt.Printf("Server: %s:%d (debug=%v, timeout=%s)\n",
c.Host, c.Port, c.Debug, c.Timeout)
}The default configurator uses both environment variables and command-line flags:
# Using environment variables
$ HOST=localhost PORT=8080 DEBUG=true TIMEOUT=30s ./app
Server: localhost:8080 (debug=true, timeout=30s)
# Using flags
$ ./app --host localhost --port 8080 --debug --timeout 30s
Server: localhost:8080 (debug=true, timeout=30s)
# Boolean flags support --no- prefix
$ ./app --host localhost --no-debug
Server: localhost: (debug=false, timeout=0s)The env provider reads from environment variables with automatic uppercasing and underscore conversion.
type Config struct {
APIKey string `env:"API_KEY"`
BaseURL string `env:"BASE_URL"`
}You can also use prefixed environment variables:
provider := env.NewProvider("MYAPP") // Looks for MYAPP_* variables
configurator := cfg.NewConfigurator(provider)The flags provider parses command-line arguments with support for short flags, equals syntax, and boolean negation.
type Config struct {
Verbose bool `flag:"verbose,v"` // --verbose or -v
Output string `flag:"output,o"` // --output file or -o file
Force bool `flag:"force"` // --force or --no-force
}Supported formats:
--flag value--flag=value-f value--flag(boolean true)--no-flag(boolean false)
Load configuration from JSON files:
file, _ := os.Open("config.json")
defer file.Close()
jsonProvider := json.NewProviderFromReader(file)
configurator := cfg.NewConfigurator(jsonProvider)The JSON provider supports nested structures using dot notation:
{
"database": {
"host": "localhost",
"port": 5432
}
}type Config struct {
DBHost string `json:"database.host"`
DBPort int `json:"database.port"`
}Provide configuration from Go values directly:
staticProvider := static.NewProvider(map[string]interface{}{
"host": "localhost",
"port": 8080,
})
configurator := cfg.NewConfigurator(staticProvider)Create your own provider by implementing the Provider interface:
type Provider interface {
StructTag() string
Provide(context.Context, string) (string, bool, error)
}Example custom provider:
type ConsulProvider struct{}
func (p *ConsulProvider) StructTag() string { return "consul" }
func (p *ConsulProvider) Provide(ctx context.Context, key string) (string, bool, error) {
// Fetch from Consul
value, err := consulClient.Get(key)
if err == ErrNotFound {
return "", false, nil
}
return value, true, err
}Providers are evaluated in order. The first provider that returns a value wins:
configurator := cfg.NewConfigurator(
flags.NewDefaultProvider(), // Check flags first
env.NewDefaultProvider(), // Then environment variables
jsonProvider, // Then JSON file
staticProvider, // Finally defaults
)The library automatically handles type conversion for:
- Primitives:
string,bool, allintandfloattypes - Time:
time.Duration(viatime.ParseDuration),time.Time(RFC3339 format) - Slices: Comma-separated values (
"a,b,c"→[]string{"a", "b", "c"}) - Maps: Key-value pairs (
"k1=v1,k2=v2"→map[string]string{"k1": "v1", "k2": "v2"}) - Nested Structs: Dot notation for nested fields
- Custom Types: Any type implementing:
json.Unmarshalerencoding.TextUnmarshalerinterface { Parse(string) error }
configurator := cfg.NewConfiguratorWithOptions(
cfg.WithProviders(myProvider),
cfg.IgnoreMissingTag, // Don't error on fields without tags
)Use the default struct tag to provide fallback values when no other provider
supplies one:
type Config struct {
Host string `env:"HOST" flag:"host" default:"localhost"`
Port int `env:"PORT" flag:"port" default:"8080"`
Verbose bool `env:"VERBOSE" default:"false"`
}The default provider has the lowest priority — any value from environment
variables, flags, or other providers takes precedence. It is included
automatically in NewDefaultConfigurator. Nested structs work as expected:
only fields with an explicit default tag receive a value.
Use the required struct tag together with the HonorRequired option to
enforce that a field receives a value from at least one provider:
type Config struct {
APIKey string `env:"API_KEY" required:"true"`
Debug bool `env:"DEBUG"`
}
configurator := cfg.NewConfiguratorWithOptions(
cfg.WithProviders(env.NewDefaultProvider()),
cfg.HonorRequired,
)
// Returns a *cfg.RequiredError if API_KEY is not set
err := configurator.Populate(ctx, &cfg)The tag accepts any truthy value (true, yes, y, 1, t).
Fields that receive a value from any provider — including the default tag —
satisfy the requirement. HonorRequired is turned on by default in
NewDefaultConfigurator.
type Database struct {
Host string `env:"DB_HOST" flag:"db-host"`
Port int `env:"DB_PORT" flag:"db-port"`
}
type Config struct {
Database Database // Automatically traversed
AppName string `env:"APP_NAME" flag:"app-name"`
}For more control, create a configurator without the default providers:
configurator := cfg.NewConfigurator(
myProvider1,
myProvider2,
)The x/cli package provides a framework for building CLI applications with automatic help generation:
import "github.com/upfluence/cfg/x/cli"
type RunConfig struct {
Host string `flag:"host,h"`
Port int `flag:"port,p"`
}
cmd := cli.StaticCommand{
Help: cli.HelpWriter(&RunConfig{}),
Synopsis: cli.SynopsisWriter(&RunConfig{}),
Execute: func(ctx context.Context, cctx cli.CommandContext) error {
var cfg RunConfig
// Configuration is automatically populated
fmt.Printf("Running on %s:%d\n", cfg.Host, cfg.Port)
return nil
},
}
app := cli.NewApp(
cli.WithName("myapp"),
cli.WithCommand(cmd),
)
app.Run(context.Background())Contributions are welcome! Please feel free to submit issues or pull requests.