Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8b5e46f
v2: module gopkg.in/ini.v2, O(1) duplicate checks for sections and keys
unknwon Nov 9, 2026
c4b6828
Replace the line parser with a single-pass lexer
unknwon Nov 9, 2026
713c446
Drop the per-section keysHash map
unknwon Nov 9, 2026
3857b9c
Functional options for loading and writing; no package globals
unknwon Nov 9, 2026
941bf95
Add Key.IsBooleanType and the KeepEmptyShadowValues option
unknwon Nov 9, 2026
0007460
Add Marshal and Unmarshal
unknwon Nov 9, 2026
dc2c2e2
README: v2 installation and migration from v1
unknwon Nov 9, 2026
6305b64
Use the unknwon.dev/ini/v2 module path
unknwon Nov 9, 2026
fbec780
Remove codecov.yml
unknwon Nov 9, 2026
e81af58
Require Go 1.26 and test on 1.26 and 1.27
unknwon Nov 9, 2026
44355e5
Fix six bugs found in review
unknwon Nov 9, 2026
241036a
Make Write output load back to the same file
unknwon Nov 9, 2026
6343dbf
Make Reload replace contents and Append read only new sources
unknwon Nov 9, 2026
f79bd1d
Stop %(name)s expansion on reference cycles and runaway growth
unknwon Nov 9, 2026
803179d
Fix panics, hangs and lost values in File, Section and Key methods
unknwon Nov 9, 2026
7ca295a
Make struct mapping return errors instead of panicking
unknwon Nov 9, 2026
edf75ee
Make concurrent use race-free and Marshal output read back
unknwon Nov 9, 2026
e336a7f
Write nested values before a later boolean line of the same key
unknwon Nov 9, 2026
a7aa752
Rename the API for v2 and replace typed accessors with generics
unknwon Nov 9, 2026
f00940b
Keep v1's list and time-layout accessors convenient
unknwon Nov 10, 2026
3d54e3f
Store sections and keys in order, and lock comments and mappers
unknwon Nov 10, 2026
cae5bf0
Accept a pointer to a struct pointer in ReflectFrom, as v1 did
unknwon Nov 10, 2026
e69b4b4
Name the formatting and quote options for what they do
unknwon Nov 10, 2026
d93a85d
Take a bool in every boolean option, as encoding/json/v2 does
unknwon Nov 10, 2026
8d0d764
Replace NoLocking(bool) with Locking(bool), and let Unmarshal's tags win
unknwon Nov 10, 2026
5388ac2
Compare the public API with v1, and read allowshadow slices as v1 did
unknwon Nov 10, 2026
b9fc952
Must* return the default without storing it
unknwon Nov 10, 2026
c4d8353
Remove the v1 comparison tests; add examples and a migration guide
unknwon Nov 10, 2026
257f3e5
Read testdata/inputs.txt with CRLF line endings
unknwon Nov 10, 2026
0453a46
Tighten the migration guide
unknwon Nov 10, 2026
d8dab3b
Remove the license headers from source files
unknwon Nov 10, 2026
67e5f78
Use no semicolons in prose
unknwon Nov 10, 2026
4fbf386
Write "e.g.," with a comma
unknwon Nov 10, 2026
561c85a
Name the lock helpers after sync.RWMutex, and correct the error docs
unknwon Nov 10, 2026
cb7534a
Give every file a counterpart
unknwon Nov 10, 2026
43f4a48
Put the shared fuzz helpers below confData
unknwon Nov 10, 2026
a16bd04
Use testify for every assertion in the tests
unknwon Nov 10, 2026
57ce7aa
Let the struct fuzzer allow trailing empty strings to drop
unknwon Nov 10, 2026
b4cfdb3
Reflow the normalizeFz comment
unknwon Nov 10, 2026
fe7480f
Name the lock helpers maybeLock, maybeUnlock, maybeRLock and maybeRUn…
unknwon Nov 10, 2026
df34b76
Make the examples cover layered config, defaults with MapTo, and stru…
unknwon Nov 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
name: Go
on:
push:
branches: [main]
branches: [main, v2]
paths:
- '**.go'
- 'go.mod'
- 'testdata/**'
- '.golangci.yml'
- '.github/workflows/go.yml'
pull_request:
paths:
- '**.go'
- 'go.mod'
- 'testdata/**'
- '.golangci.yml'
- '.github/workflows/go.yml'
env:
Expand All @@ -37,7 +39,7 @@ jobs:
name: Test
strategy:
matrix:
go-version: [1.26.x]
go-version: [1.26.x, 1.27.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
Expand Down
93 changes: 72 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,92 @@
# INI

[![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/github.com/go-ini/ini?tab=doc)
[![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/unknwon.dev/ini/v2?tab=doc)

![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200)

Package ini provides INI file read and write functionality in Go.
Package ini reads and writes INI files 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.
- Load from files, `[]byte` and `io.Reader`, merging several sources in order.
- Sections and keys kept in order, parent-child sections, auto-increment keys, repeated (shadow) keys, nested values, multiline values, and `%(name)s` references.
- Typed access to values: numbers, booleans, durations, times, and lists, ranges and candidates of any of them.
- Map files to and from structs, and `Marshal` and `Unmarshal` like `encoding/json`.
- Write files back with their comments and order. What is written always loads back the same.
- Parse errors that give the line and column.
- Safe for concurrent use.

## Installation

The minimum requirement of Go is **1.13**.
The minimum requirement of Go is **1.26**.

```sh
$ go get gopkg.in/ini.v1@latest
go get unknwon.dev/ini/v2@latest
```

> [!NOTE]
> If you previously used `github.com/go-ini/ini` as the import path in your project, without updating all of your code, you can use the following command to replace the import path in your `go.mod`:
> ```zsh
> go mod edit -replace github.com/go-ini/ini=gopkg.in/ini.v1@latest
> ```
## Getting started

## Getting Help
```go
package main

- [Getting Started](https://ini.unknwon.io/docs/intro/getting_started)
import (
"fmt"
"log"
"time"

"unknwon.dev/ini/v2"
)

func main() {
f, err := ini.Load("app.ini")
if err != nil {
log.Fatal(err) // e.g., "line 3, column 1: key-value delimiter not found: broken line"
}

server := f.Section("server")
port := server.Key("port").MustInt(8080)
timeout := server.Key("timeout").MustDuration(30 * time.Second)
fmt.Println(port, timeout)

server.Key("port").SetValue("9090")
if err := f.SaveTo("app.ini"); err != nil {
log.Fatal(err)
}
}
```

Options change the syntax that is accepted, such as `ini.Load("app.ini", ini.AllowShadows(true), ini.IgnoreInlineComment(true))`. The [package documentation](https://pkg.go.dev/unknwon.dev/ini/v2) has examples of struct mapping, typed lists and write options.

## Performance

v2 loads files up to 31 times faster than v1, with a third to a fifth of the allocations:

| Operation | v1.67.3 | v2 | Allocations, v1 → v2 |
|---|---|---|---|
| Load 10,000 keys in one section | 137 ms | 4.4 ms (31× faster) | 50.2k → 10.1k |
| Load 10,000 keys in 1,000 sections | 8.2 ms | 3.2 ms (2.5× faster) | 58.1k → 18.1k |
| Load 10,000 keys, mixed syntax | 101 ms | 3.9 ms (26× faster) | 57.6k → 18.8k |
| Write 10,000 keys | 2.2–4.3 ms | 4.3–7.4 ms (1.7–2.0× slower) | 3.0k–20.0k → 13.0k–30.0k |
| `MapTo` a 13-field struct | 3.8 µs | 4.5 µs | 44 → 48 |
| `ReflectFrom` the same struct | 13.9 µs | 11.8 µs | 132 → 118 |
| Read one key | 60 ns | 71 ns, or 36 ns with `Locking(false)` | 0 → 0 |

- **Load** is linear in the number of keys. v1 compared each new key with every key before it in its section, so a large section took quadratic time.
- **Write** is slower because it checks that every line it writes reads back as the same value. v1 could write files that loaded differently.
- **Reads** take the file's read lock so that they are safe while other goroutines change the file. v1 read without it.

Measured with Go 1.26.10 on a 4-core Intel Xeon at 2.60 GHz, as the median of 8 runs.

## Migrating from v1

Without options, v2 parses every input exactly as v1 does. The API changed: the import path is `unknwon.dev/ini/v2`, options are functions, package-level settings became options, and typed list methods became generic functions. A few behaviors changed in ways the compiler can't catch, such as `Must*` methods no longer storing their default in the file.

The [migration guide](docs/migrate-v1-to-v2.md) lists every change.

## Getting help

- [Package documentation and examples](https://pkg.go.dev/unknwon.dev/ini/v2)
- [Documentation site](https://ini.unknwon.io/docs/intro/getting_started), which still describes v1

## License

Expand Down
27 changes: 8 additions & 19 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
// Copyright 2017 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package ini

import (
"testing"

"github.com/stretchr/testify/require"
)

func newTestFile(block bool) *File {
c, _ := Load([]byte(confData))
c.BlockMode = block
var opts []LoadOption
if !block {
opts = append(opts, Locking(false))
}
c, _ := Load([]byte(confData), opts...)
return c
}

Expand Down Expand Up @@ -117,9 +108,7 @@ func Benchmark_Key_SetValue_VisSection(b *testing.B) {

func Benchmark_Key_Strings_LargeArray(b *testing.B) {
c, err := Load("testdata/large_array.ini")
if err != nil {
b.Fatal(err)
}
require.NoError(b, err)
key := c.Section("").Key("ARRAY")
b.ReportAllocs()
b.ResetTimer()
Expand Down
16 changes: 0 additions & 16 deletions codecov.yml

This file was deleted.

37 changes: 18 additions & 19 deletions data_source.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
// Copyright 2019 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package ini

import (
Expand Down Expand Up @@ -50,25 +36,38 @@ func (s *sourceData) ReadCloser() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(s.data)), nil
}

// sourceReadCloser represents an input stream with Close method.
// sourceReadCloser represents an input stream with Close method. A stream
// can be read only once, so its data is kept for reloading.
type sourceReadCloser struct {
reader io.ReadCloser
data []byte
err error
}

func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) {
return s.reader, nil
if s.reader != nil {
s.data, s.err = io.ReadAll(s.reader)
if err := s.reader.Close(); s.err == nil {
s.err = err
}
s.reader = nil
}
if s.err != nil {
return nil, s.err
}
return io.NopCloser(bytes.NewReader(s.data)), nil
}

func parseDataSource(source interface{}) (dataSource, error) {
func newDataSource(source any) (dataSource, error) {
switch s := source.(type) {
case string:
return sourceFile{s}, nil
case []byte:
return &sourceData{s}, nil
case io.ReadCloser:
return &sourceReadCloser{s}, nil
return &sourceReadCloser{reader: s}, nil
case io.Reader:
return &sourceReadCloser{io.NopCloser(s)}, nil
return &sourceReadCloser{reader: io.NopCloser(s)}, nil
default:
return nil, fmt.Errorf("error parsing data source: unknown type %q", s)
}
Expand Down
22 changes: 0 additions & 22 deletions deprecated.go

This file was deleted.

Loading
Loading