GO 1.18+ ONLY This library makes use of generics, which are only available in Go 1.18+
qcl
is a lightweight library for loading configuration values at runtime. It is designed to have a simple API, robust test suite, zero external dependencies, and be easy to integrate into existing projects. If you are looking for a more full-featured configuration library, check out Viper or Koanf. I've used both and they are great libraries, but I wanted something simpler for my use cases. I currently have no plans to support loading configuration from files, so if your use-case requires that, this library is not for you.
BE ADVISED This library is still under active development, but the API is stable and will not change before 1.0.0. The test suite is pretty extensive, but I'm sure there are still edge cases I haven't thought of. If you find a bug, please open an issue.
go get github.com/thezmc/qcl
Make sure the environment variables you want to use are set. For example:
export HOST="localhost"
export PORT="8080"
You can also use command line arguments. For example:
go run main.go --port 8081
type Config struct {
Host string
Port int
SSL bool
}
defaultConfig := Config{
Host: "anotherhost",
Port: 9090,
SSL: false,
}
conf, _ := qcl.Load(&defaultConfig)
fmt.Printf("Host: %s\n", conf.Host) // "Host: localhost" from environment
fmt.Printf("Port: %d\n", conf.Port) // "Port: 8081" from command line, overrides environment by default
fmt.Printf("SSL: %t\n", conf.SSL) // "SSL: false" from the "defaultConfig" struct
By default, the library will use the field names as the environment variable / command-line argument names. The environment variables will be loaded first, followed by the command line arguments. If a value is found in both the environment and command line arguments, the command line argument will take precedence except in the case of slice and map values.
By default, the library will look for environment variables with the same name as the struct fields, but split along word boundaries:
type Config struct {
Host string // "HOST" environment variable
DBHost string // "DB_HOST" environment variable
DBPort int // "DB_PORT" environment variable
}
You can override the environment variable name by using the env
tag:
type Config struct {
Host string `env:"HoSt"` // "HOST" environment variable
HTTPPort int `env:"PORT"` // "PORT" environment variable
}
NOTE: The override is case-insensitive. The library will convert the tag value to uppercase before looking for the environment variable.
By default, the library will look for command line arguments with the same name as the struct fields, but split along word boundaries:
type Config struct {
Host string // "--host" command line argument
DBHost string // "--db-host" command line argument
DBPort int // "--db-port" command line argument
}
You can override the command line argument name by using the flag
tag:
type Config struct {
Host string // "--host" command line argument
HTTPPort int `flag:"port"` // "--port" command line argument
}
Slices and maps are special cases when it comes to overrides. If a slice or map value is found in the environment or command-line, it will be appended to the slice or map from the default config. For example:
export HOSTS="localhost,otherhost" # separate iterable values with a comma
go run main.go --hosts "yetanotherhost"
type Config struct {
Hosts []string
}
conf, _ := qcl.Load(&Config{})
fmt.Printf("Hosts: %s\n", conf.Hosts) // "Hosts: [localhost otherhost yetanotherhost]"
Same idea for maps:
export HOSTS="localhost=8080,otherhost=9090" # separate key-value pairs with a comma, separate keys and values with an equals sign
go run main.go --hosts "yetanotherhost=1234"
type Config struct {
Hosts map[string]int
}
conf := qcl.Load(&Config{})
fmt.Printf("Hosts: %s\n", conf.Hosts) // "Hosts: map[localhost:8080 otherhost:9090 yetanotherhost:1234]"
Nested structs are also supported. The field name for the nested struct will be used as the prefix for the environment variables and command-line arguments. For example:
type Config struct {
Host string // "HOST" environment variable; "--host" command line argument
DB struct {
Host string // "DB_HOST" environment variable; "--db-host" command line argument
Port int // "DB_PORT" environment variable; "--db-port" command line argument
}
}
Embedded structs are also supported. The embedded struct will be flattened into the parent struct and so will not have a prefix. For example:
type Config struct {
User string // "USER" environment variable; "--user" command line argument
struct {
Host string // "HOST" environment variable; "--host" command line argument
Port int // "PORT" environment variable; "--port" command line argument
}
}
// is treated the same as
type Config struct {
User string // "USER" environment variable; "--user" command line argument
Host string // "HOST" environment variable; "--host" command line argument
Port int // "PORT" environment variable; "--port" command line argument
}
By default, the library doesn't expect any prefix on your environment variables. You can set a custom environment variable prefix by using the qcl.WithEnvPrefix
functional option:
type Config struct {
Host string // "MYAPP_HOST" environment variable
}
qcl.Load(&Config{}, qcl.UseEnv(qcl.WithEnvPrefix("MYAPP_"))) // the _ on the end is optional. It will be added automatically if not included.
By default, the env
struct tag is used to override environment variable names. You can set a custom environment variable struct tag by using the qcl.WithEnvStructTag
functional option:
type Config struct {
HTTPHost string `envvar:"HOST"` // "HOST" environment variable
}
qcl.Load(&Config{}, qcl.UseEnv(qcl.WithEnvStructTag("envvar")))
NOTE: The override is case-insensitive. The library will convert the tag value to uppercase before looking for the environment variable.
By default, iterables are separated by a comma. You can set a custom environment variable iterable separator by using the qcl.WithEnvSeparator
functional option:
export HOSTS="localhost|otherhost" # separate iterable values with a pipe
type Config struct {
Hosts []string // "HOSTS" environment variable
}
qcl.Load(&Config{}, qcl.UseEnv(qcl.WithEnvSeparator("|")))
You can create your own loaders
func UseJSON(path string) LoadOption {
return func(lc *qcl.LoadConfig) { // Make sure the function you return implements the qcl.LoadOption interface
// add your source to the list of sources. Be aware of the order since the sources are loaded in the order they are added.
lc.Sources = append(lc.Sources, "json")
// add your loader to the Loaders map. Be careful not to override any existing loaders: "env" and "flags" are already taken.
lc.Loaders["json"] = func(config any) error {
// do your thing...
}
}
}
NOTE: The order of the sources is important. The library will load the values from the sources in the order they are defined. If a value is found in multiple sources, the value from the last configured source will be used.
Bug reports and pull requests are welcome at the issues page. For major changes, please open an issue first to discuss what you would like to change. Any new feature PRs must include adequate test coverage and documentation. Any features importing packages that aren't in the standard library will not be accepted.