CLI tool
gookit/config avatar
gookit/config

gookit/config: one config API over eight file formats

📝 Go configuration manage(load,get,set,export). support JSON, YAML, TOML, Properties, INI, HCL, ENV and Flags. Multi file load, data override merge, parse ENV var. Go应用配置加载管理,支持多种格式,多文件加载,远程文件加载,支持数据合并,解析环境变量名

585 stars65 forksGoMIT

At a glance

What is it?
gookit/config is a Go library that loads, reads, writes and merges configuration from JSON, JSON5, YAML, TOML, INI, Properties, ENV and command line flags behind one API, with drivers loaded on demand so unused formats never reach your binary. The interesting parts are the environment variable syntax and the merge behaviour, and the limitation is that the library is global rather than scoped to an instance.
Who is it for?
gookit/config fits a Go service that reads configuration from several places at once, where a YAML file, an environment variable and a command line flag all need to land in the same key space, and where the developer does not want to pull in a full configuration framework for it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Eight formats, one package-level API

The library is described as a simple, full-featured Go application configuration management tool library, and the repository description is more explicit about what that means: configuration manage with load, get, set and export, supporting JSON, YAML, TOML, Properties, INI, HCL, ENV and Flags, with multi file load, data override merge and parse ENV var.

The formats are handled by drivers, and the driver model is the part that keeps the dependency footprint honest. The feature list says HCL needs an import of github.com/hashicorp/hcl for a custom driver, and that the other drivers are used on demand, so a driver you do not use is not loaded into the application. There is also the possibility to add a custom driver for your own format. The repository layout shows the drivers as separate directories, with json/, json5/, ini/, properties/, toml/, yaml/, yamlv3/ and other/ alongside the core files config.go, driver.go, load.go, read.go, write.go, export.go, options.go and util.go.

The licence is MIT, the module path is github.com/gookit/config/v2, it requires Go 1.21, and the recent releases are v2.2.9 from 2026-07-04, v2.2.8 from 2026-05-05 and v2.2.7 from 2025-08-15, with the last push on 2026-10-19.

Drivers you add, except JSON which is always there

The driver model is best understood from the usage example, which uses YAML and therefore has to opt in.

go
import (
    "github.com/gookit/config/v2"
    "github.com/gookit/config/v2/yaml"
)

func main() {
	// config.ParseEnv: will parse env var in string value. eg: shell: ${SHELL}
	config.WithOptions(config.ParseEnv)

	// add driver for support yaml content
	config.AddDriver(yaml.Driver)

The usage tips spell out the rules. Extra options can be added using WithOptions(), for example ParseEnv and ParseDefault. You use AddDriver() to add the required format driver, and json is loaded by default so there is no need to add it.

What that means for a deployment is worth stating directly. The dependency list in go.mod shows the format libraries as direct requirements, including BurntSushi/toml, goccy/go-json, goccy/go-yaml, titanous/json5, gookit/ini/v2 and gookit/properties, plus dario.cat/mergo for merging and go-viper/mapstructure/v2 for the struct decoding. The README's claim is about what is initialised at runtime, not about what is compiled in, and the distinction matters when you are counting binary size.

The merge library in that list, mergo, is the mechanism behind the overlay behaviour, and the feature list describes it as data overlay and merge, automatically loading by key when loading multiple copies of data.

Merging files, and the order that decides the winner

Loading is done with LoadFiles() or LoadStrings(), and the tips explain the semantics. You can pass in multiple files or call the function multiple times, and data loaded multiple times is automatically merged by key. The example makes the layering visible, loading a base file and then a second file on top of it.

go
	// load more files
	err = config.LoadFiles("testdata/yml_other.yml")
	// can also load multi at once
	// err := config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml")
	if err != nil {
		panic(err)
	}

Because the merge is by key, later files win for the keys they define, and keys they do not define survive from earlier files. That is the behaviour you want for a base file plus a per-environment file, but it also means the order of the calls is part of your configuration contract, and a key that appears in two files is resolved by call order rather than by any explicit precedence rule in the README.

The config file format itself, in the YAML example, shows the environment variable syntax at work.

yaml
name: app2
debug: false
baseKey: value2
shell: ${SHELL}
envKey1: ${NotExist|defValue}

map1:
    key: val2
    key2: val20

arr1:
    - val1
    - val21

The shell value is a plain ${SHELL} reference and envKey1 shows the default form ${NotExist|defValue}, which resolves to defValue when the variable is not set. The README states the same rule in prose: support parse ENV name and allow with default value, so envKey: ${SHELL|/bin/bash} becomes envKey: /bin/zsh.

Reading values: typed getters and key paths

The read API is a set of typed getters, and the feature list names the set: Get, Int, Uint, Int64, Float, String, Bool, Ints, IntMap, Strings, StringMap and more.

go
age := config.Int("age")
fmt.Print(age) // 100
go
val := config.Bool("debug")
fmt.Print(val) // true
go
name := config.String("name")
fmt.Print(name) // inhere

And the slice and map getters, which are how the arr1 and map1 keys from the example file are read.

go
arr1 := config.Strings("arr1")
fmt.Printf("%#v", arr1) // []string{"val1", "val21"}
go
val := config.StringMap("map1")
fmt.Printf("%#v",val) // map[string]string{"key":"val2", "key2":"val20"}

Key paths are supported for sub values, and the example shows both forms.

go
// from array
value := config.String("arr1.0")
fmt.Print(value) // "val1"

// from map
value := config.String("map1.key")
fmt.Print(value) // "val2"

The map.key and arr.2 forms in the feature list cover both nested maps and indexed arrays, and writing works the same way, with config.Set("name", "new name") updating a value in place.

Binding the whole config to a struct

The library can bind configuration data onto your own structures, and the default tag is mapstructure, which is what the README calls the default binding mapping tag of a structure. That tag name can be changed through the decoder's option options.DecoderConfig.TagName.

go
type User struct {
    Age  int  `mapstructure:"age"`
    Key  string `mapstructure:"key"`
    UserName  string `mapstructure:"user_name"`
    Tags []int  `mapstructure:"tags"`
}

user := User{}
err = config.BindStruct("user", &user)

Two features make this more than a decoder. The first is default values through struct tags, with the form default:"def_value", and the second is a default value sourced from the environment, default:"${APP_ENV | dev}", so a field can fall back to a configuration value rather than a literal.

To use a different tag name, you change the decoder option.

go
config.WithOptions(func(opt *Options) {
    options.DecoderConfig.TagName = "config"
})

And the whole configuration can be bound at once, either with config.Decode(&myConf) or with config.BindStruct("", &myConf). There is also config.MapOnExists, which behaves like BindStruct but only maps a field if the key exists in the data, which is the one to reach for when a missing key should leave the struct field at its zero value rather than being filled from a default.

Environment variables and flags land in the same key space

Two external sources feed the same store. The first is the process environment, loaded with LoadOSEnvs() and a map from the environment variable name to the config key.

go
// os env: APP_NAME=config APP_DEBUG=true DB_USERNAME=someone

// load ENV info
config.LoadOSEnvs(map[string]string{"APP_NAME": "app_name", "APP_DEBUG": "app_debug", "DB_USERNAME": "db.username"})

// read
config.Bool("app_debug") // true
config.String("app_name") // "config"

The dotted key is worth noting, because the feature list says you can set a value to a sub key in a map, so DB_USERNAME=someone becomes username inside db, which is how a flat environment variable reaches a nested configuration structure.

The second is command line flags, loaded with a simple parser where the definition format is name:type:desc or name:type or name:desc, with type and desc optional. The type can be flag type, and the allowed values are bool, int and string, with string as the default, and the desc sets the flag description.

Because all three sources, files, environment and flags, write into one key space, the precedence question has to be answered by the order you load them in, and the README does not define a global precedence. The features list mentions that the configuration data changed event is supported, with events set.value, set.data, load.data, clean.data and reload.data, so a long-running service can react to a reload, but the merging order between a file, an environment variable and a flag is your decision.

Where gookit/config is the wrong choice

Three cases point elsewhere. The first is a codebase that needs more than one independent configuration set. The API is package-level: config.Int, config.Set, config.LoadFiles and the option functions all operate on a single global store, and the examples use it that way. Two services in one process, or a test that needs a clean configuration, both push against that.

The second is a project that only needs one simple format. The README says so directly: if you just want to use INI for simple config management, the recommendation is to use gookit/ini, and it points out that the dotenv sub-package lives there rather than here, with the install command go get github.com/gookit/ini/v2/dotenv. Pulling in eight formats, a merge engine and an event system to read one INI file is a cost with no return.

The third is a project whose configuration is genuinely complex, with references between files, validation schemas or reloadable remote sources with their own semantics. The library does support loading configuration data from remote URLs and listening for changes on config data, but the feature list is a list of primitives, and there is no schema validation, no typed config with required fields, and no indication of how a remote reload interacts with the merge behaviour. The realistic alternative in that category is a framework configuration library with a real schema, or the standard library's own flag and environment handling if you can keep it simple. What gookit/config does have is a documented test suite with code coverage above 95 percent, and a single-purpose alternative for the INI case, which is what makes it a reasonable default for the middle of the range.

Editorial conclusion

gookit/config fits a Go service that reads configuration from several places at once, where a YAML file, an environment variable and a command line flag all need to land in the same key space, and where the developer does not want to pull in a full configuration framework for it. It does not fit a codebase that needs per-instance configuration objects, because the API is package-level and global, and it does not fit a project that only needs one simple format, since the README itself points to gookit/ini for that case. Before you adopt it, read the ENV syntax in the feature list, because the ${NAME|default} form is not the same as the shell expansion it resembles, and check the driver loading order, since only json is loaded by default and the rest are compiled in on demand.

Frequently asked questions

Which config formats does gookit/config support?

The feature list names JSON, which is the default and is loaded without being added, plus JSON5, INI, Properties, YAML, TOML, ENV and Flags, with HCL available through a custom driver using github.com/hashicorp/hcl. Drivers are used on demand, so a format you do not use is not loaded into the application, and you can add a custom driver for your own format.

How does gookit/config resolve environment variables in config values?

With the ParseEnv option enabled, string values are parsed for env var references, so a value of ${SHELL} expands to the environment variable, and the form ${NotExist|defValue} falls back to defValue when the variable is not set. You can also load environment variables directly into the config with LoadOSEnvs, mapping a variable name to a config key, and a dotted key such as db.username sets a value in a sub key of a map.

How do multiple config files merge in gookit/config?

You pass multiple files to LoadFiles() or call it several times, and data loaded multiple times is automatically merged by key, so a later file wins for the keys it defines and earlier keys survive. The feature list also calls this data overlay and merge, automatically loading by key when loading multiple copies of data.

Can gookit/config bind config values onto a Go struct?

Yes. BindStruct("user", &user) binds the data under a key to a struct, and Decode(&myConf) binds everything. The default tag is mapstructure, which you can change through options.DecoderConfig.TagName, and struct tags support defaults with default:"def_value" and environment-based defaults with default:"${APP_ENV | dev}". config.MapOnExists binds only when the key exists.

How do I install gookit/config, and what should I use for a simple INI file?

The install is go get github.com/gookit/config/v2, and the module requires Go 1.21. For a simple INI-only case the README recommends the gookit/ini package instead, whose dotenv sub-package can be installed with go get github.com/gookit/ini/v2/dotenv.

Official sources

  1. gookit/config on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gookit-config.svg)](https://hysenlabs.com/projects/gookit-config)

Was this useful?