Skip to content
A simple configuration library for Go

A simple configuration library for Go

June 28, 2026

Hard coding values used by a program to determine its runtime behavior (i.e., configuration variables) is a bad idea. Programs commonly provide an interface, such as a settings page or command-line interface (CLI) flags, so users can configure the program accordingly. Xenguard is no exception, but with a twist: it has its own configuration approach instead of relying on existing ones.

Introduction

A variable that determines software behavior at runtime is called a configuration variable. The set of configuration values, i.e. the assigned configuration variables, is collectively referred to as the configuration.

Configurations come in all shapes and colors, for example:

  • Windows OS has a database, the Registry, to store system and application configs.
  • Command-line Interface (CLI) allows passing options (aka flags) to define configs.
  • Firefox browser provides access to all browser config variables when navigating to the speical about:config URL.
  • NodeJS allows reading variables in KEY=VALUE format from a .env file (e.g., node --env-file=.env app.js).

Configurations are generally provided to a programm either upon initiation stage, e.g., as a CLI flag or environment variable, or the programm reads it afterwards, e.g., from a file or over the network. The decision on how to pass a configuration value depends on the stage, at which the programm needs the configuration value. For example, if the program needs values to set up networking, it obviously cannot read those values over the network. Therefore, they must be provided as environment variables or command-line options.

A Go config library

There are various solutions for managing configurations in Go: from the built-in flag package to the comprehensive viper library. For my use case, these existing solutons are either too verbose, or too bloated. To explain why, I’ll briefly state my requirements in the following, and subsequently give an overview of the implementation.

Requirements

The config library is meant for Go executables that provide a command-line interface, so the basic requirements are as follows:

  • R1: Must depend only on the Go standard library.
  • R2: Must be able to read configs from various sources:
    • R2.1: From CLI flags.
    • R2.2: From environment variables.
    • R2.3: From files.
  • R3: Must be able to accept same value over all sources.
  • R4: Must be able to handle a value if defined over multiple sources simultaneously.
  • R5: Must be able to parse configs to basic types (e.g., string, int, etc.).

I specifically didn’t want to introduce any new dependencies, thus requirement R1, which also implies the config file (R2.3) must be in a format natively support by Go (e.g, json and not yaml).

R2 caters for flexibility. For example, in a development environment, it’s okay to pass the database password as a CLI flag or persist it in a file. However, in a production environment, such as a containerized one, I’d prefer to pass the password as an environment variable (e.g., a Podman secret).

R3 caters for a unified interface, so the user is not constrained in how to define config values. And if a value is defined through different sources, e.g., both as a CLI flag and environment variable, the library should choose one deterministically (R4).

Finally, the library has to take care of parsing values into Go types (R5). I deliberately decided against complex types such as arrays or maps to keep it simple. A simple key-value data structure can also be used to represent more complex structures.

Implementation

The implementation is just under 100 lines of code (at the time of writing).

The easiest way to register a config variable is the following function:

func RegisterNewConfigItem[T configType](name, desc string, value T) (c *ConfigItem[T], err error)

For example:

package main

import "codeberg.org/Xenguard/config-manager/config"

var answer = config.RegisterNewConfigItem[int]("answer", "answer to the universe", -1)

func main() {
	config.Parse()

	// All variants below are equivalent:
	// $ CONFIG_ANSWER=42 ./main
	// $ ./main -answer 42
	// $ CONFIG_ANSWER=0 ./main -answer 42
	fmt.Println(answer.Value())
	// Output: 42
}

It uses flag package and os.Getenv to read values from the CLI and the environment respectively (R2.1 and R.2.2). Variables can also be persisted in an environment file (technically fulfilling R2.3), but they must be exported in current process environment (e.g., source config.env) to take effect. This way there is no need for the library to implement file handling. CLI flags take precedence over environment variables (R3 and R4). And finally, it can parse basic type (boolean, numeric, and string) based on the generic type provided at registration (R5).

Final Thoughts

I already use this Library in Xenguard components, but I’m not finished with the development. There are some aspects that needs improving. For example, debugging: if multiple sources set the value for the same variable, how can the client know which source was used?

As I integrate the config library in other components, I’m going to come up with other features that I’d like to implement. If you are developing Go programms with command-line interfaces, you might want to give the library a go and let me know what you think,

Last updated on