[Awesome Go] Use ini to read and write INI file in Golang (Go)

ini

Package ini provides INI file read and write functionality in Go.

Features

  • Load from multiple data sources(file, []byte, io.Reader and io.ReadCloser) with overwrites.

  • Read with recursion values.

  • Read with parent-child sections.

  • Read with auto-increment key names.

  • Read with multiple-line values.

  • Read with tons of helper methods.

  • Read and convert values to Go types.

  • Read and WRITE comments of sections and keys.

  • Manipulate sections, keys and comments with ease.

  • Keep sections and keys in order as you parse and save.

Installation

Standard go get:

1
$ go get -u gopkg.in/ini.v1

Getting Started

We will go through a very simple example to illustrate how to get started.

First of all, create two files (my.ini and main.go) under the directory of your choice, let’s say we choose /tmp/ini.

1
2
3
4
5
6
7
8
9
$ mkdir -p /tmp/ini
$ cd /tmp/ini
$ touch my.ini main.go
$ tree .
.
├── main.go
└── my.ini

0 directories, 2 files

Now, we put some content into the my.ini file (partially take from Grafana).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# possible values : production, development
app_mode = development

[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
data = /home/git/grafana

[server]
# Protocol (http or https)
protocol = http

# The http port to use
http_port = 9999

# Redirect to correct domain if host header does not match domain
# Prevents DNS rebinding attacks
enforce_domain = true

Great, let’s start writing some code in main.go to manipulate this file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"os"

"gopkg.in/ini.v1"
)

func main() {
cfg, err := ini.Load("my.ini")
if err != nil {
fmt.Printf("Fail to read file: %v", err)
os.Exit(1)
}

// Classic read of values, default section can be represented as empty string
fmt.Println("App Mode:", cfg.Section("").Key("app_mode").String())
fmt.Println("Data Path:", cfg.Section("paths").Key("data").String())

// Let's do some candidate value limitation
fmt.Println("Server Protocol:",
cfg.Section("server").Key("protocol").In("http", []string{"http", "https"}))
// Value read that is not in candidates will be discarded and fall back to given default value
fmt.Println("Email Protocol:",
cfg.Section("server").Key("protocol").In("smtp", []string{"imap", "smtp"}))

// Try out auto-type conversion
fmt.Printf("Port Number: (%[1]T) %[1]d\n", cfg.Section("server").Key("http_port").MustInt(9999))
fmt.Printf("Enforce Domain: (%[1]T) %[1]v\n", cfg.Section("server").Key("enforce_domain").MustBool(false))

// Now, make some changes and save it
cfg.Section("").Key("app_mode").SetValue("production")
cfg.SaveTo("my.ini.local")
}

Almost there, let’s run this program and check the output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ go run main.go
App Mode: development
Data Path: /home/git/grafana
Server Protocol: http
Email Protocol: smtp
Port Number: (int) 9999
Enforce Domain: (bool) true

$ cat my.ini.local
# possible values : production, development
app_mode = production

[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
data = /home/git/grafana
...

Perfect! Though the example is very basic and covered only a small bit of all functionality, but it’s a good start.

Map between struct and section

Map To Struct

Want more objective way to play with INI? Cool.

1
2
3
4
5
6
7
8
Name = Unknwon
age = 21
Male = true
Born = 1993-01-01T20:17:05Z

[Note]
Content = Hi is a good man!
Cities = HangZhou, Boston
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Note struct {
Content string
Cities []string
}

type Person struct {
Name string
Age int `ini:"age"`
Male bool
Born time.Time
Note
Created time.Time `ini:"-"`
}

func main() {
cfg, err := ini.Load("path/to/ini")
// ...
p := new(Person)
err = cfg.MapTo(p)
// ...

// Things can be simpler.
err = ini.MapTo(p, "path/to/ini")
// ...

// Just map a section? Fine.
n := new(Note)
err = cfg.Section("Note").MapTo(n)
// ...
}

Can I have default value for field? Absolutely.

Assign it before you map to struct. It will keep the value as it is if the key is not presented or got wrong type.

1
2
3
4
5
// ...
p := &Person{
Name: "Joe",
}
// ...

It’s really cool, but what’s the point if you can’t give me my file back from struct?

Reflect From Struct

Why not?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Embeded struct {
Dates []time.Time `delim:"|" comment:"Time data"`
Places []string `ini:"places,omitempty"`
None []int `ini:",omitempty"`
}

type Author struct {
Name string `ini:"NAME"`
Male bool
Age int `comment:"Author's age"`
GPA float64
NeverMind string `ini:"-"`
*Embeded `comment:"Embeded section"`
}

func main() {
a := &Author{"Unknwon", true, 21, 2.8, "",
&Embeded{
[]time.Time{time.Now(), time.Now()},
[]string{"HangZhou", "Boston"},
[]int{},
}}
cfg := ini.Empty()
err = ini.ReflectFrom(cfg, a)
// ...
}

So, what do I get?

1
2
3
4
5
6
7
8
9
10
11
NAME = Unknwon
Male = true
; Author's age
Age = 21
GPA = 2.8

; Embeded section
[Embeded]
; Time data
Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
places = HangZhou,Boston

Map with ShadowLoad

If you want to map a section to a struct along with ShadowLoad, then you need to indicate allowshadow in the struct tag.

For example, suppose you have the following configuration:

1
2
3
4
[IP]
value = 192.168.31.201
value = 192.168.31.211
value = 192.168.31.221

You should define your struct as follows:

1
2
3
type IP struct {
Value []string `ini:"value,omitempty,allowshadow"`
}

In case you don’t need the first two tag rules, then you can just have ini:",,allowshadow".

Other Notes On Map/Reflect

Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Child struct {
Age string
}

type Parent struct {
Name string
Child
}

type Config struct {
City string
Parent
}

Example configuration:

1
2
3
4
5
6
7
City = Boston

[Parent]
Name = Unknwon

[Child]
Age = 21

What if, yes, I’m paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome.

1
2
3
4
5
6
7
8
9
10
11
12
13
type Child struct {
Age string
}

type Parent struct {
Name string
Child `ini:"Parent"`
}

type Config struct {
City string
Parent
}

Example configuration:

1
2
3
4
5
City = Boston

[Parent]
Name = Unknwon
Age = 21

See also Customize name and value mappers - https://ini.unknwon.io/docs/advanced/name_and_value_mapper.

References

[1] GitHub - go-ini/ini: Package ini provides INI file read and write functionality in Go. - https://github.com/go-ini/ini

[2] go-ini/ini: A fantastic package for INI manipulations in Go - https://ini.unknwon.io/

[3] ini · pkg.go.dev - https://pkg.go.dev/github.com/go-ini/ini