Go library that integrates config loading from YAML files and Go flags.
mflag was implemented based on a typical workflow for apps running in Kubernetes:
- In production, read configs from a configmap YAML file
- In staging, read default configs
- In local, read default configs with the option of overriding them via CLI flags, for ease of debugging.
- Simple, Declarative API - Define defaults, load a file, and parse. That's it.
- Automatic Command-line Flags - Every configuration key is automatically available as a command-line flag for overrides.
- YAML configuration support - Load defaults from config files
- Clear precedence order - Command-line flags > Config file > Code defaults
- Minimal dependencies - Only requires YAML parsing
go get github.com/ndealmeida/mflag
main.go:
package main
import (
"fmt"
"log"
"github.com/ndealmeida/mflag"
)
func defaults() {
mflag.SetDefault("debug", true)
mflag.SetDefault("port", 3000)
mflag.SetDefault("database.host", "localhost")
mflag.SetDefault("database.port", 5432)
mflag.SetDefault("database.name", "myapp")
}
func main() {
defaults()
if err := mflag.Init("configmap.yaml"); err != nil {
log.Fatalf("Error loading config: %v", err)
}
mflag.Parse()
// Print all flags
if mflag.GetBool("debug") {
mflag.Debug()
}
}
configmap.yaml:
port: 3000
debug: false
database:
host: "localhost"
port: 5432
name: "myapp"
You can override any config via CLI:
go run main.go --help
go run main.go --port=8080
Reading from yaml is optional and won't return an error if the file doesn't exist. Hence it is a good practise to always provide safe defaults.
Values are resolved in this order (highest to lowest priority):
- Command-line flags - Explicit user input (highest priority)
- YAML configuration file - Persistent settings
- Default values in code - Fallback values
After calling mflag.Parse()
, you can retrieve values by key:
mflag.Parse()
// Get typed values
port := mflag.GetInt("port")
debug := mflag.GetBool("debug")
// Use dot-notation to access nested values
host := mflag.GetString("database.host")
// or via map if there are several keys within the map
// and they all have string values
dbKey := mflag.GetStringMapString("database")
host = dbKey["host"]
// Check if a value was explicitly set
if mflag.IsSet("port") {
fmt.Println("Port was explicitly configured")
}
Check example for a practical example of parsing configs into a struct. In bigger applications, you may want to split AppConfig
into multiple configs like DBConfig
, CacheConfig
, etc.
This library is for you if you want a lightweight and ergonomic API. This library is NOT for you if you require strong config validation at every step, and relying on sane defaults is not an option for your use case. See the examples below:
As mentioned above, we deliberately chose to not return an error when the config YAML file is not found. This allows an application to run with defaults locally without needing a config file. In any other case, such as a file with wrong permissions or invalid YAML syntax, Init()
will return a descriptive error.
This library prioritizes ease of use over forcing error checks on every value retrieval. In the example above:
port := mflag.GetInt("port")
host := mflag.GetString("host")
debug := mflag.GetBool("debug")
When the flag was not found, Get* functions return their zero value. If we would have opted for a stronger validation, we would need to check errors in every single line of loading each config, which would be verbose and tedious.
Parse()
does not return an error, to be consistent with the flag
package from the standard library. It is designed for simplicity in common use cases. If an error occurs during parsing (e.g., an invalid value in a config file), Parse()
will print the error message and exit the application, mirroring the default behavior of flag.Parse()
.
Loading configurations is often the first thing an application does, so this is generally an acceptable approach. However if you open connections or spin up go routines before loading configs, exiting won't gracefully shutdown your application. For those cases you can use ParseWithError()
. This function performs the same logic as Parse() but returns an error on failure instead of exiting.
Note: calling any Get* function before Parse() or ParseWithError() will cause a panic. This is a deliberate design choice to prevent silent failures from incorrect library usage, distinguishing a programmer error (violating the library's lifecycle) from a runtime error (bad input data).
Contributions are welcome! Please feel free to submit a Pull Request.