Persisting Application Configuration In Golang

I often observe that configuration is overlooked in books and posts devoted to application development. Authors only slightly touch it at best. I believe it’s quite an important topic on its own and deserves a dedicated post. So in this post you will find explained examples of how to make persistent configurations in Go applications.

I will demonstrate how to work with configuration for two most popular formats: json and yaml. In examples we will store and load config of hypothetical cluster manager captured with the following structs:

type Cluster struct {
	Name       string
	DataCentre string
	Nodes      []string
}

type Configuration struct {
	Clusters    []Cluster
	MinReplicas int
	MaxReplicas int
}
JSON configuration

If you never worked with json before, it is a compact human readable format proposed by Java Script community as an alternative to more verbose xml. Json took software development industry by storm and is the most popular data transfer format nowadays. Below is how our cluster manager configuration may look like in json:

{
  "clusters": [
    {
      "name": "Dev",
      "datacentre": "Local",
      "nodes": [
        "dev1.company.com",
        "dev2.company.com"
      ]
    },
    {
      "name": "Prod",
      "datacentre": "Amazon",
      "nodes": [
        "prd1.company.com",
        "prd2.company.com",
        "prd3.company.com"
      ]
    }
  ],
  "min_replicas": 1,
  "max_replicas": 5
}

Standard package json implements serialisation and deserialisation of Go structs to and from json string. Function Marshal serialises struct into byte slice, and function Unmarshal deserialises slice of bytes into struct. Code snippet below exemplifies how application configuration stored in Go structs can be easily persisted to and restored from a json file.

type Cluster struct {
	Name       string   `json:"name"`
	DataCentre string   `json:"datacentre"`
	Nodes      []string `json:"nodes"`
}

type Configuration struct {
	Clusters    []Cluster `json:"clusters"`
	MinReplicas int       `json:"min_replicas"`
	MaxReplicas int       `json:"max_replicas"`
}

func saveConfig(c Configuration, filename string) error {
	bytes, err := json.MarshalIndent(c, "", "  ")
	if err != nil {
		return err
	}

	return ioutil.WriteFile(filename, bytes, 0644)
}

func loadConfig(filename string) (Configuration, error) {
	bytes, err := ioutil.ReadFile(filename)
	if err != nil {
		return Configuration{}, err
	}

	var c Configuration
	err = json.Unmarshal(bytes, &c)
	if err != nil {
		return Configuration{}, err
	}

	return c, nil
}

There are a couple of points to note. First, struct fields are annotated with json tags, e.g.

Name       string   `json:"name"`

It’s not strictly necessary, but is a good practice to follow, because package json uses the tags to determine field names in json string.

Second, instead of Marshal I used MarshalIndent method, which generates pretty multi-line string which is much easier to read by humans. This could be handy if you need to print json string or store it in a file (as I do in the example).

YAML configuration

YAML is a compact human readable format. It was designed to be easily mapped to data types common to most high-level languages: lists, associative arrays, and scalars. This is how our cluster manager configuration may look like in YAML:

---
clusters:
- name: Dev
  datacentre: Local
  nodes:
  - dev1.company.com
  - dev2.company.com
- name: Prod
  datacentre: Amazon
  nodes:
  - prd1.company.com
  - prd2.company.com
  - prd3.company.com
min_replicas: 1
max_replicas: 5

Package YAML, similar to json, has functions Marshal and Unmarshal to serialise/deserialise Go struct to/from a byte slice. Code snippet below shows how our application configuration can be persisted to and restored from a YAML file.

type Cluster struct {
	Name       string   `yaml:"name"`
	DataCentre string   `yaml:"datacentre"`
	Nodes      []string `yaml:"nodes"`
}

type Configuration struct {
	Clusters    []Cluster `yaml:"clusters"`
	MinReplicas int       `yaml:"min_replicas"`
	MaxReplicas int       `yaml:"max_replicas"`
}

func saveConfig(c Configuration, filename string) error {
	bytes, err := yaml.Marshal(c)
	if err != nil {
		return err
	}

	return ioutil.WriteFile(filename, bytes, 0644)
}

func loadConfig(filename string) (Configuration, error) {
	bytes, err := ioutil.ReadFile(filename)
	if err != nil {
		return Configuration{}, err
	}

	var c Configuration
	err = yaml.Unmarshal(bytes, &c)
	if err != nil {
		return Configuration{}, err
	}

	return c, nil
}

Note that, similar to json example, struct fields are annotated with YAML tags, e.g.

Name       string   `yaml:"name"`

Again, it’s not mandatory, but is a good practice to follow for exactly the same reason.

So which format to choose?

If you asked me three years ago, my answer would be json. Nowadays, however, YAML becomes a standard de facto for storing complex configurations. So if your configuration is or going to be non-trivial, go with YAML. For a simple one, choose what you like.