Developing applications in Golang, I've tried numerous Config managers, but unfortunately, none of them fully satisfied my needs:
- Simplicity and ease of use; really as simple as possible.
- Struct unmarshalling.
- Struct tags usage.
- Customization and injection of custom parsers and value providers as freely as possible while maintaining ease of use.
- Time spent on deploying a new configuration:
- Creating a configuration contract
- Setting default values
- Setting constraints
- etc., up to obtaining a ready-to-use configuration.
Don't get me wrong, most of this is implemented in existing Config managers, but unfortunately - partially, not all at once.
So I embarked on reinventing the wheel, doing my best to ensure it wouldn't end up square.
Meet the GoCfg
Jagerente / gocfg
⚙️ Golang config manager. Control your configurations using tags, unmarshal to structs, implement and inject your own value providers and parsers.
GoCfg
Key Features
- Unmarshal from Environment Variables, .env and any other sources right to your structs.
- Set default values for each field using tags.
- Easy to inject as much custom parsers as you need.
- Easy to inject your own values providers as much as you need and use them all at once with priority.
- Automatic documentation generator.
Quick start
Install package:
go get -u github.com/Jagerente/gocfg
Basic usage:
It will use environment variables and default values defined in tags.
package main
import (
"github.com/Jagerente/gocfg"
"github.com/Jagerente/gocfg/pkg/parsers"
"github.com/Jagerente/gocfg/pkg/values"
"time"
)
type LoggerConfig struct {
LogLevel string `env:"LOG_LEVEL" default:"debug"`
}
type RedisConfig struct {
RedisHost string `env:"REDIS_HOST" default:"localhost"`
RedisPort uint16 `env:"REDIS_PORT" default:"6379"`
RedisUser string `env:"REDIS_USER,omitempty"`
RedisPassword string `env:"REDIS_PASS"`
RedisDatabase string `env:"REDIS_DATABASE"`
}
type AppConfig struct {
// Supported Tags:
// - env: Specifies the environment variable name.
// - default: Specifies the default value for the field.
// - omitempty: Allows empty fields.
…I don't want to "beat around the bush", so I'll just familiarize you with the functionality of my library and try to explain why it's convenient and why you should try it.
What's inside
- Define the configuration contract using Golang Structures and Tags:
- Variable name
- Default value
- Permission not to set the variable value
- Description for documentation.
- Unmarshal from Environment Variables, .env files, or implement, for example your own Custom Value Provider for YAML and inject it.
- Custom Value Parsers
- Not satisfied with the default Value Parsers? Want the value 10 to be parsed into the number 83 during unmarshalling? Or maybe you want to parse your own type, like time.Duration? It's covered by gocfg!
- Automatic Documentation Generation based on the contract.
Quick view
- Start by importing
go get -u github.com/Jagerente/gocfg
. - Create an internal configuration package, for example -
internal/config/config.go
.
package config
import (
"github.com/Jagerente/gocfg"
"github.com/Jagerente/gocfg/pkg/values"
"time"
)
type Config struct {
}
func New() (*Config, error) {
var cfg = new(Config)
cfgManager := gocfg.NewDefault()
if err := cfgManager.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}
Looks pretty simple, doesn't it?
- Let's create a configuration contract by filling in the Config structure:
type LoggerConfig struct {
LogLevel int `env:"LOG_LEVEL" default:"6" description:"Possible values:\n0 - Trace\n1 - Debug\n2 - Info\n3 - Error"`
ReportCaller bool `env:"REPORT_CALLER" default:"true"`
LogFormatter int `env:"LOG_FORMATTER" default:"0"`
}
type CassandraConfig struct {
CassandraHosts string `env:"CASSANDRA_HOSTS" default:"127.0.0.1"`
CassandraKeyspace string `env:"CASSANDRA_KEYSPACE" default:"messenger"`
}
type RouterConfig struct {
ServerPort uint16 `env:"SERVER_PORT" default:"8080"`
Debug bool `env:"ROUTER_DEBUG" default:"true"`
CacheAdapter string `env:"CACHE_ADAPTER,omitempty" description:"Leave blank to not use.\nPossible values:\n- redis\n- memcache"`
CacheAdapterTTL time.Duration `env:"CACHE_ADAPTER_TTL,omitempty" default:"1m"`
CacheAdapterNoCacheParam string `env:"CACHE_ADAPTER_NOCACHE_PARAM,omitempty" default:"no-cache"`
}
type Config struct {
LoggerConfig `title:"Logger configuration"`
RouterConfig `title:"Router configuration"`
CassandraConfig `title:"Cassandra configuration"`
}
I believe that the tag names are informative and intuitive enough, but just in case, let's go through them:
-
env
- Specifies the variable name in the configuration file and is necessary for Value Provider implementations to map Key-Value. -
default
- Specifies the default value of the variable. -
omitempty
- Means that the variable can be left undeclared, and then the default value will be used, or if the default is missing, the field will take Golang's zero value; for example,0
forint
type orfalse
forbool
type. -
description
- The field is used during automatic documentation generation. It is necessary to provide a description of the variable. -
title
- The field is used during automatic documentation generation. It is necessary to separate groups of variables; a group consists of variables in a separate structure, for example,LoggerConfig
is a separate group of variables.
- Let's go back to the functionality and inspect it:
func New() (*Config, error) {
var cfg = new(Config)
cfgManager := gocfg.NewDefault()
if err := cfgManager.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}
Code above is equivalent to the following:
func New() (*Config, error) {
var cfg = new(Config)
cfgManager := gocfg.NewEmpty().
UseDefaults().
AddParserProviders(parsers.NewDefaultParserProvider()).
AddValueProviders(values.NewEnvProvider())
if err := cfgManager.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}
-
UseDefaults()
- Enables the use of default values from the default tag. -
AddParserProviders()
- Sets value parsers.- The following types are supported by default parsers:
time.Duration
bool
string
int, int8, int16, int32, int64
uint, uint8, uint16, uint32, uint64
float32, float64
-
AddValueProviders()
- Sets configuration providers. -
values.NewEnvProvider()
- Parser for Environment Variables.- Not to be confused with .env file!
.ENV files parser
Let's add parsing values from the .env file, the driver is available out of the box:
func New() (*Config, error) {
// With default '.env' file
dotEnvProvider, _ := values.NewDotEnvProvider()
// With custom env file path
dotEnvProvider, _ = values.NewDotEnvProvider("local.env")
// With multiple env files
dotEnvProvider, _ = values.NewDotEnvProvider("local.env", "dev.env")
var cfg = new(Config)
cfgManager := gocfg.NewEmpty().
UseDefaults().
AddParserProviders(parsers.NewDefaultParserProvider()).
AddValueProviders(
values.NewEnvProvider(),
dotEnvProvider,
)
if err := cfgManager.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}
You might be wondering:
What will happen when using multiple .env files?
Values will be read and written in the order from the first added to the last added file. Suppose the first file has 5/10 variables, and the second has 7/10, then the second file will overwrite 2 intersecting values that were already written from the first file, and the missing ones will simply be written.
Or:
What about the priority of value providers, when you add multiple?
When attempting to retrieve a value from all value providers in the order from the first added to the last added, the first non-empty value will be returned.
Different Key Tag name
All you need is add .UseCustomKeyTag("mapstructure")
:
func New() (*Config, error) {
dotEnvProvider, _ := values.NewDotEnvProvider()
var cfg = new(Config)
cfgManager := gocfg.NewEmpty().
UseDefaults().
AddParserProviders(parsers.NewDefaultParserProvider()).
AddValueProviders(
values.NewEnvProvider(),
dotEnvProvider,
).
UseCustomKeyTag("mapstructure")
if err := cfgManager.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}
Do not forget to update our contract:
type LoggerConfig struct {
LogLevel int `mapstructure:"LOG_LEVEL" default:"6" description:"Possible values:\n0 - Trace\n1 - Debug\n2 - Info\n3 - Error"`
ReportCaller bool `mapstructure:"REPORT_CALLER" default:"true"`
LogFormatter int `mapstructure:"LOG_FORMATTER" default:"0"`
}
type CassandraConfig struct {
CassandraHosts string `mapstructure:"CASSANDRA_HOSTS" default:"127.0.0.1"`
CassandraKeyspace string `mapstructure:"CASSANDRA_KEYSPACE" default:"messenger"`
}
type RouterConfig struct {
ServerPort uint16 `mapstructure:"SERVER_PORT" default:"8080"`
Debug bool `mapstructure:"ROUTER_DEBUG" default:"true"`
CacheAdapter string `mapstructure:"CACHE_ADAPTER,omitempty" description:"Leave blank to not use.\nPossible values:\n- redis\n- memcache"`
CacheAdapterTTL time.Duration `mapstructure:"CACHE_ADAPTER_TTL,omitempty" default:"1m"`
CacheAdapterNoCacheParam string `mapstructure:"CACHE_ADAPTER_NOCACHE_PARAM,omitempty" default:"no-cache"`
}
type Config struct {
LoggerConfig `title:"Logger configuration"`
RouterConfig `title:"Router configuration"`
CassandraConfig `title:"Cassandra configuration"`
}
Want to use values ONLY from default tags?
No problem, let's add .ForceDefaults()
func New() (*Config, error) {
dotEnvProvider, _ := values.NewDotEnvProvider()
var cfg = new(Config)
cfgManager := gocfg.NewEmpty().
UseDefaults().
AddParserProviders(parsers.NewDefaultParserProvider()).
AddValueProviders(
values.NewEnvProvider(),
dotEnvProvider,
).
UseCustomKeyTag("mapstructure").
ForceDefaults()
if err := cfgManager.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}
Custom Parser Provider
Nothing special, just an example of already implemented time.Duration parser.
type CustomParserProvider struct {
}
func NewCustomParserProvider() *CustomParserProvider {
return &CustomParserProvider{}
}
func (p *CustomParserProvider) Get(field reflect.Value) (func(v string) (any, error), bool) {
switch field.Type() {
case reflect.TypeOf(time.Duration(83)):
return func(v string) (any, error) {
return time.ParseDuration(v)
}, true
default:
return nil, false
}
}
func New() (*Config, error) {
customParserProvider := NewCustomParserProvider()
var cfg = new(Config)
cfgManager := gocfg.NewDefault().
AddParserProviders(customParserProvider)
if err := cfgManager.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}
Custom Value Provider
Nothing special as well, let's just implement provider that will read environment variables with a prefix:
type CustomValueProvider struct {
}
func NewCustomValueProvider() *CustomValueProvider {
return &CustomValueProvider{}
}
func (p *CustomValueProvider) Get(key string) string {
return os.Getenv("CUSTOM_" + key)
}
func New() (*Config, error) {
customValueProvider := NewCustomValueProvider()
var cfg = new(Config)
cfgManager := gocfg.NewDefault().
AddValueProviders(customValueProvider)
if err := cfgManager.Unmarshal(cfg); err != nil {
return nil, err
}
return cfg, nil
}
Documentation Generation
You can do it just in 2 steps using copy & paste dev power.
- Create app
/cmd/docs/main.go
:
package main
import (
"fmt"
"github.com/Jagerente/gocfg"
"github.com/Jagerente/gocfg/pkg/docgens"
"os"
"your_cool_app/internal/config"
)
const outputFile = ".env.dist.generated"
func main() {
cfg := new(config.Config)
file, err := os.Create(outputFile)
if err != nil {
panic(fmt.Errorf("error creating %s file: %v", outputFile, err))
}
cfgManager := gocfg.NewDefault()
if err := cfgManager.GenerateDocumentation(cfg, docgens.NewEnvDocGenerator(file)); err != nil {
panic(err)
}
}
- Run it with
go run cmd/docs/main.go
; after which the file.env.dist.generated
will be generated:
# Auto-generated config
#############################
# Logger configuration
#############################
# Description:
# Possible values:
# 0 - Trace
# 1 - Debug
# 2 - Info
# 3 - Error
LOG_LEVEL=6
REPORT_CALLER=true
LOG_FORMATTER=0
#############################
# Router configuration
#############################
SERVER_PORT=8080
ROUTER_DEBUG=true
# Allowed to be empty
# Description:
# Leave blank to not use.
# Possible values:
# - redis
# - memcache
CACHE_ADAPTER=
# Allowed to be empty
CACHE_ADAPTER_TTL=1m
# Allowed to be empty
CACHE_ADAPTER_NOCACHE_PARAM=no-cache
#############################
# Cassandra configuration
#############################
CASSANDRA_HOSTS=127.0.0.1
CASSANDRA_KEYSPACE=messenger
This is my very first open-source library, as well as this article, so If you've reached this point, I'm truly grateful to you. I'd like to ask you to share the most important thing - criticism! I also won't refuse kind words, stars on GitHub repository and any kind of support in promoting this library. <3
This project is open to contributions. If you wish to add your own implementation of a Value Provider driver or Documentation Generator, or perhaps you'd like to conduct a code review or suggest improvements, feel free to submit a pull request or contact me!
Top comments (1)
Nice, will give it a try