Using a struct (and one optional description map) as single source of truth to add configuration to Go applications.
The approach is simplistic on purpose, supporting only flat configuration structures and only four field types: string, int, float64, bool.
If you need more features there are probably dozens of proper configuration libraries for Go.
go get github.com/smartfactory-kl/appgofigBasic Example:
package main
import (
"log"
"github.com/smartfactory-kl/appgofig"
)
// Define the Config struct itself
type Config struct {
AppName string `default:"my-app"`
Port int `default:"8080"`
Debug bool `default:"false"`
Ratio float64 `default:"1.0"`
}
func main() {
// Read config using the configuration type.
// With no sources, it will only apply the defined default values
cfg, err := appgofig.ReadConfig[Config]()
if err != nil {
log.Fatal(err)
}
// Config options are autocompleted
log.Println(cfg.AppName, cfg.Port, cfg.Debug, cfg.Ratio)
// Usually, at least one source should be used, for example the environment
cfgFromEnv, err := appgofig.ReadConfig[Config](
appgofig.WithSources(
appgofig.EnvironmentSource(),
),
)
if err != nil {
log.Fatal(err)
}
// Config options are still autocompleted
log.Println(cfgFromEnv.AppName, cfgFromEnv.Port, cfgFromEnv.Debug, cfgFromEnv.Ratio)
}Most metadata is set using struct tags.
Sets the initial value for a field. Needs to be the string representation.
Port int `default:"8080"`
Sets the Environment Key to look for. If omitted, the field name is converted to UPPER_SNAKE_CASE and used as Environment Key.
type Config struct {
AppName string `env:"APPLICATION_NAME"` // will be looking for APPLICATION_NAME
HTTPPort int // will be looking for HTTP_PORT
}Note
Note that an optional Environment Prefix will still be added to both variants
Setting this to true requires a field to not be empty before type conversion.
Note
Empty values for booleans are considered to be false
APIKey string `required:"true"`Setting this to true masks the value and the default value of the field when using VisitConfigEntries and in any generated documentation.
APIKey string `masked:"true"`Note
The value will be replaced with [Masked (len: x)] with x being the byte length of the (stringified) value or default value
Specifying no sources will result in a Config with only default values.
To specify a source, use the WithSources() option.
Two variants are available:
// Using no Prefix
appgofig.WithSources(
appgofig.EnvironmentSource()
)
// Using a Prefix on all Keys
appgofig.WithSources(
appgofig.PrefixedEnvironmentSource("MY_PREFIX")
)For a field tagged with env:"APPLICATION_NAME" this would result in the following keys to look up:
- EnvironmentSource ->
APPLICATION_NAME - PrefixedEnvironmentSource ->
MY_PREFIX_APPLICATION_NAME
The YAMLSource can either use predefined default file paths or a single specified one:
// Using the first match of the defaults
appgofig.WithSources(
appgofig.YAMLSource()
)
// Using a single file specified by its path
appgofig.WithSources(
appgofig.SpecificYAMLSource("config.dev.yml")
)Note
SpecificYAMLSource will return an error if the file is not readable - but YAMLSource will not error if none of the default files is found and simply return no values
The default file paths are (in order)
config.ymlconfig.yamlconfig/config.ymlconfig/config.yaml
YAML Keys must match the Go struct field name exactly and it must contain a flat hierarchy. Nested objects are not supported.
AppName: yaml-app
Port: 9000
Debug: trueAnother option is WithOverrides, containing a map[string]string that will always be applied last.
appgofig.WithOverrides(map[string]string{
"AppVersion": "1.0.0-rc4",
})Sources and Overrides can be combined. Sources will be applied in order, Overrides always at the end. If multiple sources define the same key, later sources will overwrite earlier ones.
cfg, err := appgofig.ReadConfig[Config](
appgofig.WithSources(
appgofig.YAMLSource(),
appgofig.PrefixedEnvironmentSource("APP"),
),
appgofig.WithOverrides(map[string]string{
"Port": "7000",
}),
)Using VisitConfigEntries, all configuration values can be inspected:
err := appgofig.VisitConfigEntries(
cfg,
func(entry appgofig.AppConfigEntry) {
log.Printf("%s=%s", entry.Key, entry.Value)
},
)Note
Masked values will be reported as [Masked (len: N)]
AppConfigEntry includes:
type AppConfigEntry struct {
Key string
Value string
ValueType reflect.Kind
DefaultValue string
IsRequired bool
IsMasked bool
EnvironmentKey string
}descriptions := map[string]string{
"AppName": "The application name.",
"Port": "The listening port.",
}
if err := appgofig.CreateConfigDocumentation(
cfg,
descriptions,
"ENV_PREFIX",
"docs",
); err != nil {
log.Fatal(err)
}This will generate:
docs/DefaultDocumentation.mddocs/config.example.yaml
The Markdown document contains a configuration overview, Docker Compose example, and Docker run example.
They can also be created individually:
err := appgofig.CreateConfigMarkdownDocument(
cfg,
descriptions,
"ENV_PREFIX",
"docs/config.md",
)
err := appgofig.CreateConfigExampleYAML(
cfg,
descriptions,
"ENV_PREFIX",
"docs/config.example.yaml",
)Note
ENV_PREFIX should only be used if one of the sources is the prefixed environment. Otherwise simply use ""
Custom sources implement the AppGofigSource interface:
type AppGofigSource interface {
Load(map[string]*appgofig.AppConfigEntry) (map[string]string, error)
}The returned map should use configuration struct field names as keys. Unknown keys are ignored. The input map parameter is for informational purposes only and should never be altered (e.g. reading the EnvironmentKey or its ValueType)
Run tests:
go test ./... -coverCreate coverage HTML report:
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html