Go Config

Go Config is a pluggable dynamic config library

Overview

Most config in applications are statically configured or include complex logic to load from multiple sources. Go-config makes this easy, pluggable and mergeable. You’ll never have to deal with config in the same way again.

Features

  • Dynamic Loading - Load configuration from multiple source as and when needed. Go Config manages watching config sourcesin the background and automatically merges and updates an in memory view.

  • Pluggable Sources - Choose from any number of sources to load and merge config. The backend source is abstracted away into a standard format consumed internally and decoded via encoders. Sources can be env vars, flags, file, etcd, k8s configmap, etc.

  • Mergeable Config - If you specify multiple sources of config, regardless of format, they will be merged and presented in a single view. This massively simplifies priority order loading and changes based on environment.

  • Observe Changes - Optionally watch the config for changes to specific values. Hot reload your app using Go Config’s watcher.You don’t have to handle ad-hoc hup reloading or whatever else, just keep reading the config and watch for changes if you need to be notified.

  • Safe Recovery - In case config loads badly or is completely wiped away for some unknown reason, you can specify fallback values when accessing any config values directly. This ensures you’ll always be reading some sane default in the event of a problem.

Getting Started

  • Source - A backend from which config is loaded
  • Encoder - Handles encoding/decoding source config
  • Reader - Merges multiple encoded sources as a single format
  • Config - Config manager which manages multiple sources
  • Usage - Example usage of go-config
  • FAQ - General questions and answers
  • TODO - TODO tasks/features

Sources

A Source is a backend from which config is loaded. Multiple sources can be used at the same time.

The following sources are supported:

  • cli - read from parsed CLI flags
  • consul - read from consul
  • env - read from environment variables
  • etcd - read from etcd v3
  • file - read from file
  • flag - read from flags
  • memory - read from memory

There are also community-supported plugins, which support the following sources:

  • configmap - read from k8s configmap
  • grpc - read from grpc server
  • runtimevar - read from Go Cloud Development Kit runtime variable
  • url - read from URL
  • vault - read from Vault server

TODO:

  • git url

ChangeSet

Sources return config as a ChangeSet. This is a single internal abstraction for multiple backends.

  1. type ChangeSet struct {
  2. // Raw encoded config data
  3. Data []byte
  4. // MD5 checksum of the data
  5. Checksum string
  6. // Encoding format e.g json, yaml, toml, xml
  7. Format string
  8. // Source of the config e.g file, consul, etcd
  9. Source string
  10. // Time of loading or update
  11. Timestamp time.Time
  12. }

Encoder

An Encoder handles source config encoding/decoding. Backend sources may store config in many different formats. Encoders give us the ability to handle any format. If an Encoder is not specified it defaults to json.

The following encoding formats are supported:

  • json
  • yaml
  • toml
  • xml
  • hcl

Reader

A Reader represents multiple changesets as a single merged and queryable set of values.

  1. type Reader interface {
  2. // Merge multiple changeset into a single format
  3. Merge(...*source.ChangeSet) (*source.ChangeSet, error)
  4. // Return return Go assertable values
  5. Values(*source.ChangeSet) (Values, error)
  6. // Name of the reader e.g a json reader
  7. String() string
  8. }

The reader makes use of Encoders to decode changesets into map[string]interface{} then merge them into a single changeset. It looks at the Format field to determine the Encoder. The changeset is then represented as a set of Values with the ability to retrive Go types and fallback where values cannot be loaded.

  1. // Values is returned by the reader
  2. type Values interface {
  3. // Return raw data
  4. Bytes() []byte
  5. // Retrieve a value
  6. Get(path ...string) Value
  7. // Return values as a map
  8. Map() map[string]interface{}
  9. // Scan config into a Go type
  10. Scan(v interface{}) error
  11. }

The Value interface allows casting/type asserting to go types with fallback defaults.

  1. type Value interface {
  2. Bool(def bool) bool
  3. Int(def int) int
  4. String(def string) string
  5. Float64(def float64) float64
  6. Duration(def time.Duration) time.Duration
  7. StringSlice(def []string) []string
  8. StringMap(def map[string]string) map[string]string
  9. Scan(val interface{}) error
  10. Bytes() []byte
  11. }

Config

Config manages all config, abstracting away sources, encoders and the reader.

It manages reading, syncing, watching from multiple backend sources and represents them as a single merged and queryable source.

  1. // Config is an interface abstraction for dynamic configuration
  2. type Config interface {
  3. // provide the reader.Values interface
  4. reader.Values
  5. // Stop the config loader/watcher
  6. Close() error
  7. // Load config sources
  8. Load(source ...source.Source) error
  9. // Force a source changeset sync
  10. Sync() error
  11. // Watch a value for changes
  12. Watch(path ...string) (Watcher, error)
  13. }

Usage

Sample Config

A config file can be of any format as long as we have an Encoder to support it.

Example json config:

  1. {
  2. "hosts": {
  3. "database": {
  4. "address": "10.0.0.1",
  5. "port": 3306
  6. },
  7. "cache": {
  8. "address": "10.0.0.2",
  9. "port": 6379
  10. }
  11. }
  12. }

New Config

Create a new config (or just make use of the default instance)

  1. import "github.com/micro/go-micro/v2/config"
  2. conf := config.NewConfig()

Load File

Load config from a file source. It uses the file extension to determine config format.

  1. import (
  2. "github.com/micro/go-micro/v2/config"
  3. )
  4. // Load json config file
  5. config.LoadFile("/tmp/config.json")

Load a yaml, toml or xml file by specifying a file with the appropriate file extension

  1. // Load yaml config file
  2. config.LoadFile("/tmp/config.yaml")

If an extension does not exist, specify the encoder

  1. import (
  2. "github.com/micro/go-micro/v2/config"
  3. "github.com/micro/go-micro/v2/config/source/file"
  4. )
  5. enc := toml.NewEncoder()
  6. // Load toml file with encoder
  7. config.Load(file.NewSource(
  8. file.WithPath("/tmp/config"),
  9. source.WithEncoder(enc),
  10. ))

Read Config

Read the entire config as a map

  1. // retrieve map[string]interface{}
  2. conf := config.Map()
  3. // map[cache:map[address:10.0.0.2 port:6379] database:map[address:10.0.0.1 port:3306]]
  4. fmt.Println(conf["hosts"])

Scan the config into a struct

  1. type Host struct {
  2. Address string `json:"address"`
  3. Port int `json:"port"`
  4. }
  5. type Config struct{
  6. Hosts map[string]Host `json:"hosts"`
  7. }
  8. var conf Config
  9. config.Scan(&conf)
  10. // 10.0.0.1 3306
  11. fmt.Println(conf.Hosts["database"].Address, conf.Hosts["database"].Port)

Read Values

Scan a value from the config into a struct

  1. type Host struct {
  2. Address string `json:"address"`
  3. Port int `json:"port"`
  4. }
  5. var host Host
  6. config.Get("hosts", "database").Scan(&host)
  7. // 10.0.0.1 3306
  8. fmt.Println(host.Address, host.Port)

Read individual values as Go types

  1. // Get address. Set default to localhost as fallback
  2. address := config.Get("hosts", "database", "address").String("localhost")
  3. // Get port. Set default to 3000 as fallback
  4. port := config.Get("hosts", "database", "port").Int(3000)

Watch Path

Watch a path for changes. When the file changes the new value will be made available.

  1. w, err := config.Watch("hosts", "database")
  2. if err != nil {
  3. // do something
  4. }
  5. // wait for next value
  6. v, err := w.Next()
  7. if err != nil {
  8. // do something
  9. }
  10. var host Host
  11. v.Scan(&host)

Multiple Sources

Multiple sources can be loaded and merged. Merging priority is in reverse order.

  1. config.Load(
  2. // base config from env
  3. env.NewSource(),
  4. // override env with flags
  5. flag.NewSource(),
  6. // override flags with file
  7. file.NewSource(
  8. file.WithPath("/tmp/config.json"),
  9. ),
  10. )

Set Source Encoder

A source requires an encoder to encode/decode data and specify the changeset format.

The default encoder is json. To change the encoder to yaml, xml, toml specify as an option.

  1. e := yaml.NewEncoder()
  2. s := consul.NewSource(
  3. source.WithEncoder(e),
  4. )

Add Reader Encoder

The reader uses encoders to decode data from sources with different formats.

The default reader supports json, yaml, xml, toml and hcl. It represents the merged config as json.

Add a new encoder by specifying it as an option.

  1. e := yaml.NewEncoder()
  2. r := json.NewReader(
  3. reader.WithEncoder(e),
  4. )

FAQ

How is this different from Viper?

Viper and go-config are solving the same problem. Go-config provides a different interface and is part of the larger micro ecosystem of tooling.

What’s the difference between Encoder and Reader?

The encoder is used by a backend source to encode/decode it’s data. The reader uses encoders to decode data from multiple sources with different formats, it then merges them into a single encoding format.

In the case of a file source , we use the file extension to determine the config format so the encoder is not used.

In the case of consul, etcd or similar key-value source we may load from a prefix containing multiple keys which means the source needs to understand the encoding so it can return a single changeset.

In the case of environment variables and flags we also need a way to encode the values as bytes and specify the format so it can later be merged by the reader.

Why is changeset data not represented as map[string]interface{}?

In some cases source data may not actually be key-value so it’s easier to represent it as bytes and defer decoding to the reader.