Skip to main content

ini - INI Configuration File Parser

ini is a powerful INI configuration file parser based on go-ini, with enhanced environment variable injection functionality. It supports multiple data sources, type conversion, variable references, environment variable substitution, and other advanced features.

Installation

go get -u go-slim.dev/ini

Quick Start

Basic Usage

package main

import (
"fmt"
"log"
"go-slim.dev/ini"
)

func main() {
// Create INI manager and load file
cfg := ini.New(ini.Options{})
if err := cfg.Append("config.ini"); err != nil {
log.Fatal(err)
}

// Read configuration values
section := cfg.Section("database")
host := section.String("host")
port := section.MustInt("port", 3306)

fmt.Printf("Database: %s:%d\n", host, port)
}

INI File Example

# config.ini

# Application configuration
[app]
name = MyApp
debug = true
port = 8080

# Database configuration
[database]
host = localhost
port = 3306
username = root
password = secret
max_connections = 100

# Redis configuration
[redis]
host = localhost
port = 6379
database = 0

Core Concepts

1. Manager

Manager is the core management object for INI configuration, responsible for loading, parsing, and managing all configuration data.

2. Section

Section represents a configuration section in the INI file (marked with [section_name]).

3. Key

Key represents a key-value pair within a configuration section.

Hierarchical Structure

Manager
└── Section 1
├── Key 1
├── Key 2
└── ...
└── Section 2
├── Key 1
└── ...

Core Features

1. Creating a Manager

New - Create New Manager

func New(opts Options) *Manager

Description:

Creates a new INI manager with the specified options.

Parameters:

  • opts: Configuration options (see Options description)

Example:

// Use default options
cfg := ini.New(ini.Options{})

// Custom options
cfg := ini.New(ini.Options{
Insensitive: true, // Case-insensitive key names
AllowBooleanKeys: true, // Allow boolean type keys
IgnoreInlineComment: false, // Do not ignore inline comments
})

2. Loading Data Sources

Append - Append Data Source

func (m *Manager) Append(source any, others ...any) error

Description:

Appends one or more data sources and automatically reloads. Supports multiple data source types.

Supported Data Source Types:

  • string: File path
  • []byte: Byte slice
  • io.Reader: Reader
  • io.ReadCloser: Closable reader
  • DataSource: Custom data source interface
  • func() (io.ReadCloser, error): Data source factory function

Example:

cfg := ini.New(ini.Options{})

// Load from file
cfg.Append("config.ini")

// Load from multiple files
cfg.Append("base.ini", "production.ini", "local.ini")

// Load from byte slice
data := []byte(`
[app]
name = MyApp
port = 8080
`)
cfg.Append(data)

// Load from io.Reader
file, _ := os.Open("config.ini")
cfg.Append(file)

Batch - Batch Operations

func (m *Manager) Batch(fn func(m *Manager) error) error

Description:

Executes multiple operations in batch without triggering auto-reload during execution. All operations are loaded together after completion.

Example:

cfg := ini.New(ini.Options{})

err := cfg.Batch(func(m *ini.Manager) error {
// Load multiple files in batch, reload only once at the end
if err := m.Append("base.ini"); err != nil {
return err
}
if err := m.Append("override.ini"); err != nil {
return err
}
return nil
})

Reload - Reload

func (m *Manager) Reload() error

Description:

Reloads and reparses all data sources. Clears existing data and reparses.

Example:

// Reload after modifying configuration file
if err := cfg.Reload(); err != nil {
log.Fatal(err)
}

3. Section Operations

Section - Get Section

func (m *Manager) Section(name string) *Section

Description:

Gets the section with the specified name. Returns a zero-value section if it doesn't exist (no error).

Example:

// Get section
dbSection := cfg.Section("database")
appSection := cfg.Section("app")

// Default section (part without section name at the beginning of the file)
defaultSection := cfg.Section("")

GetSection - Get Section (with Error Check)

func (m *Manager) GetSection(name string) (*Section, error)

Description:

Gets the section with the specified name. Returns an error if it doesn't exist.

Example:

section, err := cfg.GetSection("database")
if err != nil {
log.Printf("Section not found: %v", err)
return
}

HasSection - Check if Section Exists

func (m *Manager) HasSection(name string) bool

Description:

Checks if the specified section exists.

Example:

if cfg.HasSection("database") {
// Database configuration exists
}

NewSection - Create New Section

func (m *Manager) NewSection(name string) *Section

Description:

Creates a new section. Returns the existing section if it already exists.

Example:

// Create new section
section := cfg.NewSection("cache")
section.NewKey("driver", "redis")
section.NewKey("host", "localhost")

4. Key Operations

Key - Get Key

func (s *Section) Key(name string) *Key

Description:

Gets the key with the specified name. Returns a zero-value key if it doesn't exist (no error).

Example:

section := cfg.Section("database")
hostKey := section.Key("host")
portKey := section.Key("port")

GetKey - Get Key (with Error Check)

func (s *Section) GetKey(name string) (*Key, error)

Description:

Gets the key with the specified name. Returns an error if it doesn't exist.

Supports Sub-section Lookup: If the key is not found in the current section, it will look up in the parent section.

Example:

section := cfg.Section("database")
key, err := section.GetKey("host")
if err != nil {
log.Printf("Key not found: %v", err)
return
}

HasKey - Check if Key Exists

func (s *Section) HasKey(name string) bool

Example:

section := cfg.Section("database")
if section.HasKey("password") {
// Password configuration exists
}

NewKey - Create New Key

func (s *Section) NewKey(name, value string) *Key

Description:

Creates a new key-value pair in the section. Returns the existing key if it already exists.

Example:

section := cfg.NewSection("app")
section.NewKey("name", "MyApp")
section.NewKey("version", "1.0.0")

Keys - Get All Keys

func (s *Section) Keys() []*Key

Description:

Returns all keys in the section.

Example:

section := cfg.Section("database")
for _, key := range section.Keys() {
fmt.Printf("%s = %s\n", key.Name(), key.String())
}

5. Value Reading - Basic Types

String - Get String Value

func (s *Section) String(name string) string
func (k *Key) String() string

Example:

// Read from Section
host := cfg.Section("database").String("host")

// Read from Key
key := cfg.Section("database").Key("host")
value := key.String()

MustString - Get String Value (with Default)

func (s *Section) MustString(name string, defaultVal ...string) string
func (k *Key) MustString(defaultVal string) string

Example:

// Returns default if key doesn't exist or is empty
host := cfg.Section("database").MustString("host", "localhost")

Int/Int64/Uint/Uint64 - Get Integer Values

func (s *Section) Int(name string) (int, error)
func (s *Section) Int64(name string) (int64, error)
func (s *Section) Uint(name string) (uint, error)
func (s *Section) Uint64(name string) (uint64, error)

Example:

port, err := cfg.Section("database").Int("port")
if err != nil {
log.Printf("Invalid port: %v", err)
}

maxConns, _ := cfg.Section("database").Uint("max_connections")

MustInt/MustInt64/MustUint/MustUint64 - Get Integer Values (with Default)

func (s *Section) MustInt(name string, defaultVal ...int) int
func (s *Section) MustInt64(name string, defaultVal ...int64) int64
func (s *Section) MustUint(name string, defaultVal ...uint) uint
func (s *Section) MustUint64(name string, defaultVal ...uint64) uint64

Example:

// Returns default if conversion fails or doesn't exist
port := cfg.Section("app").MustInt("port", 8080)
timeout := cfg.Section("server").MustInt64("timeout", 30)
maxSize := cfg.Section("upload").MustUint("max_size", 1024)

Float64 - Get Float Value

func (s *Section) Float64(name string) (float64, error)
func (s *Section) MustFloat64(name string, defaultVal ...float64) float64

Example:

rate, err := cfg.Section("app").Float64("tax_rate")
ratio := cfg.Section("app").MustFloat64("compression_ratio", 0.8)

Bool - Get Boolean Value

func (s *Section) Bool(name string) (bool, error)
func (s *Section) MustBool(name string, defaultVal ...bool) bool

Supported Boolean Values:

  • true: "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On"
  • false: "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off"

Example:

debug, err := cfg.Section("app").Bool("debug")
verbose := cfg.Section("app").MustBool("verbose", false)

Duration - Get Time Duration

func (s *Section) Duration(name string) (time.Duration, error)
func (s *Section) MustDuration(name string, defaultVal ...time.Duration) time.Duration

Example:

// config.ini
// [server]
// timeout = 30s
// interval = 5m

timeout, err := cfg.Section("server").Duration("timeout")
interval := cfg.Section("server").MustDuration("interval", 1*time.Minute)

Time - Get Time Value

func (s *Section) Time(name string) (time.Time, error)
func (s *Section) TimeFormat(name string, format string) (time.Time, error)
func (s *Section) MustTime(name string, defaultVal ...time.Time) time.Time
func (s *Section) MustTimeFormat(name string, format string, defaultVal ...time.Time) time.Time

Example:

// config.ini
// [app]
// created_at = 2023-12-25T10:30:45Z
// updated_at = 2023/12/25 10:30:45

// RFC3339 format
createdAt, err := cfg.Section("app").Time("created_at")

// Custom format
updatedAt, err := cfg.Section("app").TimeFormat("updated_at", "2006/01/02 15:04:05")

// With default
defaultTime := time.Now()
lastLogin := cfg.Section("user").MustTime("last_login", defaultTime)

6. List Value Reading

Strings - Get String List

func (s *Section) Strings(name string, delim string) []string
func (k *Key) Strings(delim string) []string

Description:

Splits the value using the specified delimiter and returns a string list.

Example:

// config.ini
// [app]
// allowed_origins = http://localhost:3000,https://example.com,https://api.example.com
// tags = go, web, api

origins := cfg.Section("app").Strings("allowed_origins", ",")
// []string{"http://localhost:3000", "https://example.com", "https://api.example.com"}

tags := cfg.Section("app").Strings("tags", ",")
// []string{"go", "web", "api"} (auto-trimmed spaces)

Ints/Int64s/Uints/Uint64s - Get Integer Lists

func (s *Section) Ints(name string, delim string) []int
func (s *Section) Int64s(name string, delim string) []int64
func (s *Section) Uints(name string, delim string) []uint
func (s *Section) Uint64s(name string, delim string) []uint64

Description:

Splits the value and converts to an integer list. Invalid values are converted to zero.

Example:

// config.ini
// [app]
// ports = 8080,8081,8082
// retry_delays = 1,2,5,10

ports := cfg.Section("app").Ints("ports", ",")
// []int{8080, 8081, 8082}

delays := cfg.Section("app").Int64s("retry_delays", ",")
// []int64{1, 2, 5, 10}

ValidInts/ValidInt64s - Get Valid Integer Lists

func (s *Section) ValidInts(name string, delim string) []int
func (s *Section) ValidInt64s(name string, delim string) []int64

Description:

Splits the value and converts to an integer list. Invalid values are skipped (not included in the result).

Example:

// config.ini
// [app]
// ports = 8080,invalid,8081,8082

ports := cfg.Section("app").Ints("ports", ",")
// []int{8080, 0, 8081, 8082} (invalid converted to 0)

validPorts := cfg.Section("app").ValidInts("ports", ",")
// []int{8080, 8081, 8082} (invalid skipped)

StrictInts/StrictInt64s - Get Strict Integer Lists

func (s *Section) StrictInts(name string, delim string) ([]int, error)
func (s *Section) StrictInt64s(name string, delim string) ([]int64, error)

Description:

Splits the value and converts to an integer list. Returns an error on the first invalid value.

Example:

// config.ini
// [app]
// ports = 8080,8081,8082
// bad_ports = 8080,invalid,8082

ports, err := cfg.Section("app").StrictInts("ports", ",")
// []int{8080, 8081, 8082}, nil

badPorts, err := cfg.Section("app").StrictInts("bad_ports", ",")
// nil, error (returns error when encountering "invalid")

Float64s/Bools/Times - Other Type Lists

// Float lists
func (s *Section) Float64s(name string, delim string) []float64
func (s *Section) ValidFloat64s(name string, delim string) []float64
func (s *Section) StrictFloat64s(name string, delim string) ([]float64, error)

// Boolean lists
func (s *Section) Bools(name string, delim string) []bool
func (s *Section) ValidBools(name string, delim string) []bool
func (s *Section) StrictBools(name string, delim string) ([]bool, error)

// Time lists
func (s *Section) Times(name string, delim string) []time.Time
func (s *Section) TimesFormat(name string, format, delim string) []time.Time
func (s *Section) ValidTimes(name string, delim string) []time.Time
func (s *Section) StrictTimes(name string, delim string) ([]time.Time, error)

Example:

// config.ini
// [app]
// rates = 0.1,0.2,0.3
// features = true,false,true,false
// timestamps = 2023-01-01T00:00:00Z,2023-06-01T00:00:00Z

rates := cfg.Section("app").Float64s("rates", ",")
features := cfg.Section("app").Bools("features", ",")
timestamps := cfg.Section("app").Times("timestamps", ",")

7. Value Validation and Range Checking

In - Check if Value is in Candidate List

func (s *Section) In(name string, defaultVal string, candidates []string) string
func (s *Section) InInt(name string, defaultVal int, candidates []int) int
func (s *Section) InFloat64(name string, defaultVal float64, candidates []float64) float64

Description:

Checks if the value is in the given candidate list. Returns the default if not.

Example:

// config.ini
// [app]
// log_level = debug
// port = 8080

// Only allow specific log levels
logLevel := cfg.Section("app").In("log_level", "info",
[]string{"debug", "info", "warn", "error"})
// Returns "debug"

invalidLevel := cfg.Section("app").In("invalid_level", "info",
[]string{"debug", "info", "warn", "error"})
// Key doesn't exist, returns default "info"

// Only allow specific ports
port := cfg.Section("app").InInt("port", 8080,
[]int{8080, 8081, 8082})
// Returns 8080

Range - Range Check

func (s *Section) RangeInt(name string, defaultVal, min, max int) int
func (s *Section) RangeInt64(name string, defaultVal, min, max int64) int64
func (s *Section) RangeFloat64(name string, defaultVal, min, max float64) float64
func (s *Section) RangeTime(name string, defaultVal, min, max time.Time) time.Time

Description:

Checks if the value is within the given range (inclusive). Returns the default if out of range.

Example:

// config.ini
// [server]
// port = 8080
// workers = 4
// rate_limit = 0.95

// Port must be between 1024-65535
port := cfg.Section("server").RangeInt("port", 8080, 1024, 65535)
// Returns 8080 (within range)

badPort := cfg.Section("server").RangeInt("bad_port", 8080, 1024, 65535)
// Assuming bad_port = 100, returns default 8080 (out of range)

// Worker count must be between 1-16
workers := cfg.Section("server").RangeInt("workers", 4, 1, 16)

// Rate limit must be between 0.0-1.0
rateLimit := cfg.Section("server").RangeFloat64("rate_limit", 0.8, 0.0, 1.0)

Validate - Custom Validation

func (s *Section) Validate(name string, fn func(string) string) string
func (k *Key) Validate(fn func(string) string) string

Description:

Validates and potentially modifies the key value using a custom function.

Example:

// config.ini
// [app]
// url = HTTP://EXAMPLE.COM/API

// Convert to lowercase
url := cfg.Section("app").Validate("url", func(value string) string {
return strings.ToLower(value)
})
// Returns "http://example.com/api"

// Ensure ends with slash
apiBase := cfg.Section("app").Validate("api_base", func(value string) string {
if !strings.HasSuffix(value, "/") {
return value + "/"
}
return value
})

8. Variable References and Environment Variables

Variable References

INI files support referencing other key values using the %(key)s syntax.

Example:

# config.ini
[paths]
root = /var/www
data = %(root)s/data
logs = %(root)s/logs
cache = %(root)s/cache

[database]
host = localhost
port = 3306
dsn = %(host)s:%(port)s
cfg := ini.New(ini.Options{})
cfg.Append("config.ini")

// Automatically resolves references
dataPath := cfg.Section("paths").String("data")
// Returns "/var/www/data"

logsPath := cfg.Section("paths").String("logs")
// Returns "/var/www/logs"

dsn := cfg.Section("database").String("dsn")
// Returns "localhost:3306"

Cross-Section References:

If the referenced key is not found in the current section, it will automatically look in the default section (unnamed section):

# config.ini
# Default section
root = /var/www

[app]
data_dir = %(root)s/data
dataDir := cfg.Section("app").String("data_dir")
// Returns "/var/www/data" (root retrieved from default section)

Environment Variable Injection

This is the main enhancement of go-slim.dev/ini compared to the original go-ini.

Supports two syntax patterns for injecting environment variables:

Syntax 1: ${VAR||default} - Empty Fallback

Uses the default when the environment variable doesn't exist or is empty.

Syntax 2: ${VAR??default} - Non-existent Fallback

Uses the default only when the environment variable doesn't exist (empty strings are considered valid).

Example:

# config.ini
[database]
# Read from environment variable, use default if doesn't exist
host = ${DB_HOST||localhost}
port = ${DB_PORT||3306}
username = ${DB_USER||root}
password = ${DB_PASSWORD}

[app]
# Force fallback: use default even if env var exists but is empty
api_key = ${API_KEY||default_key}

# Weak fallback: use default only if doesn't exist
feature_flag = ${FEATURE_FLAG??true}
// Set environment variables
os.Setenv("DB_HOST", "prod-db.example.com")
os.Setenv("DB_PORT", "5432")
os.Setenv("FEATURE_FLAG", "") // Empty string

cfg := ini.New(ini.Options{})
cfg.Append("config.ini")

// Read configuration
host := cfg.Section("database").String("host")
// Returns "prod-db.example.com" (from environment variable)

port := cfg.Section("database").MustInt("port", 3306)
// Returns 5432 (from environment variable)

username := cfg.Section("database").String("username")
// Returns "root" (env var not set, use default)

// || vs ??
apiKey := cfg.Section("app").String("api_key")
// If API_KEY is empty string, returns "default_key"

featureFlag := cfg.Section("app").String("feature_flag")
// Returns "" (env var exists but is empty, ?? doesn't use default)

Quotes in Default Values:

Default values can be wrapped in single or double quotes:

[app]
message = ${WELCOME_MSG||'Hello, World!'}
api_url = ${API_URL||"https://api.example.com"}

9. Child Sections

Child sections create a hierarchical relationship using dot notation.

Example:

# config.ini
[server]
host = 0.0.0.0

[server.http]
port = 8080
timeout = 30s

[server.https]
port = 8443
timeout = 60s
cert = /path/to/cert.pem
cfg := ini.New(ini.Options{
ChildSectionDelimiter: ".", // Default is "."
})
cfg.Append("config.ini")

// Access child sections
httpSection := cfg.Section("server.http")
httpPort := httpSection.MustInt("port", 80)

httpsSection := cfg.Section("server.https")
httpsPort := httpsSection.MustInt("port", 443)

// Child sections inherit keys from parent sections
// If server.http doesn't have host, it will look in server
host := httpSection.String("host")
// Returns "0.0.0.0" (inherited from parent section server)

Getting Parent Section:

httpsSection := cfg.Section("server.https")
parentSection, hasParent := httpsSection.Parent()
if hasParent {
fmt.Println(parentSection.Name()) // "server"
}

Options Configuration

type Options struct {
// Loose indicates whether the parser ignores non-existent files
Loose bool

// Insensitive forces all section and key names to lowercase
Insensitive bool

// InsensitiveSections forces all section names to lowercase
InsensitiveSections bool

// InsensitiveKeys forces all key names to lowercase
InsensitiveKeys bool

// IgnoreContinuation ignores continuation lines
IgnoreContinuation bool

// IgnoreInlineComment ignores inline comments
IgnoreInlineComment bool

// AllowBooleanKeys allows boolean type keys (value is true)
// Mainly for my.cnf type configuration files
AllowBooleanKeys bool

// AllowPythonMultilineValues allows Python-style multiline values
AllowPythonMultilineValues bool

// SpaceBeforeInlineComment requires space before comment symbol
SpaceBeforeInlineComment bool

// UnescapeValueDoubleQuotes unescapes quotes in double-quoted values
UnescapeValueDoubleQuotes bool

// UnescapeValueCommentSymbols unescapes comment symbols
UnescapeValueCommentSymbols bool

// KeyValueDelimiters key-value delimiters, default "=:"
KeyValueDelimiters string

// ChildSectionDelimiter child section delimiter, default "."
ChildSectionDelimiter string

// PreserveSurroundedQuote preserves quotes around values
PreserveSurroundedQuote bool

// ReaderBufferSize reader buffer size (bytes)
ReaderBufferSize int

// AllowNonUniqueSections allows sections with the same name
AllowNonUniqueSections bool

// AllowDuplicateShadowValues allows duplicate shadow values
AllowDuplicateShadowValues bool

// Mutex concurrency lock (customizable)
Mutex Mutex

// Transformer custom value transformer
Transformer ValueTransformer
}

Common Configuration Examples

// Loose mode: ignore non-existent files
cfg := ini.New(ini.Options{
Loose: true,
})

// Case-insensitive
cfg := ini.New(ini.Options{
Insensitive: true,
})

// Allow boolean keys (MySQL configuration style)
cfg := ini.New(ini.Options{
AllowBooleanKeys: true,
})

// Ignore inline comments
cfg := ini.New(ini.Options{
IgnoreInlineComment: true,
})

// Custom delimiters
cfg := ini.New(ini.Options{
KeyValueDelimiters: "=", // Only allow = as delimiter
ChildSectionDelimiter: "::", // Use :: as child section delimiter
})

Use Cases

1. Application Configuration Management

// config.ini
// [app]
// name = MyApp
// version = 1.0.0
// debug = ${DEBUG||false}
// port = ${PORT||8080}
//
// [database]
// host = ${DB_HOST||localhost}
// port = ${DB_PORT||3306}
// name = ${DB_NAME||mydb}
// user = ${DB_USER||root}
// password = ${DB_PASSWORD}

type Config struct {
App AppConfig
Database DatabaseConfig
}

type AppConfig struct {
Name string
Version string
Debug bool
Port int
}

type DatabaseConfig struct {
Host string
Port int
Name string
User string
Password string
}

func LoadConfig() (*Config, error) {
cfg := ini.New(ini.Options{})
if err := cfg.Append("config.ini"); err != nil {
return nil, err
}

config := &Config{
App: AppConfig{
Name: cfg.Section("app").String("name"),
Version: cfg.Section("app").String("version"),
Debug: cfg.Section("app").MustBool("debug", false),
Port: cfg.Section("app").MustInt("port", 8080),
},
Database: DatabaseConfig{
Host: cfg.Section("database").String("host"),
Port: cfg.Section("database").MustInt("port", 3306),
Name: cfg.Section("database").String("name"),
User: cfg.Section("database").String("user"),
Password: cfg.Section("database").String("password"),
},
}

return config, nil
}

2. Multi-Environment Configuration

// Load base configuration + environment-specific configuration
cfg := ini.New(ini.Options{Loose: true})

// Load in batch
err := cfg.Batch(func(m *ini.Manager) error {
// Base configuration
m.Append("config.ini")

// Environment-specific configuration
env := os.Getenv("APP_ENV")
if env != "" {
m.Append(fmt.Sprintf("config.%s.ini", env))
}

// Local override configuration
m.Append("config.local.ini")

return nil
})

3. Service Configuration with Environment Variables

// config.ini
// [redis]
// host = ${REDIS_HOST||localhost}
// port = ${REDIS_PORT||6379}
// password = ${REDIS_PASSWORD}
// database = ${REDIS_DB||0}
// max_retries = 3
// pool_size = ${REDIS_POOL_SIZE||10}

func NewRedisClient() (*redis.Client, error) {
cfg := ini.New(ini.Options{})
if err := cfg.Append("config.ini"); err != nil {
return nil, err
}

section := cfg.Section("redis")

client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
section.String("host"),
section.MustInt("port", 6379)),
Password: section.String("password"),
DB: section.MustInt("database", 0),
MaxRetries: section.MustInt("max_retries", 3),
PoolSize: section.MustInt("pool_size", 10),
})

return client, nil
}

4. Dynamic Configuration Reload

type ConfigManager struct {
cfg *ini.Manager
watchers []func(*ini.Manager)
mu sync.RWMutex
}

func NewConfigManager(filename string) (*ConfigManager, error) {
cfg := ini.New(ini.Options{})
if err := cfg.Append(filename); err != nil {
return nil, err
}

return &ConfigManager{
cfg: cfg,
watchers: make([]func(*ini.Manager), 0),
}, nil
}

func (cm *ConfigManager) Reload() error {
cm.mu.Lock()
defer cm.mu.Unlock()

if err := cm.cfg.Reload(); err != nil {
return err
}

// Notify all watchers
for _, watcher := range cm.watchers {
watcher(cm.cfg)
}

return nil
}

func (cm *ConfigManager) Watch(fn func(*ini.Manager)) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.watchers = append(cm.watchers, fn)
}

func (cm *ConfigManager) GetInt(section, key string, defaultVal int) int {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.cfg.Section(section).MustInt(key, defaultVal)
}

5. Complex Configuration Validation

// config.ini
// [server]
// host = 0.0.0.0
// port = 8080
// workers = 4
// timeout = 30s
//
// [limits]
// max_connections = 1000
// rate_limit = 100
// request_size = 10485760

func ValidateConfig(cfg *ini.Manager) error {
// Validate port range
port := cfg.Section("server").MustInt("port", 8080)
if port < 1024 || port > 65535 {
return fmt.Errorf("invalid port: %d (must be 1024-65535)", port)
}

// Validate worker count
workers := cfg.Section("server").MustInt("workers", 4)
if workers < 1 || workers > 256 {
return fmt.Errorf("invalid workers: %d (must be 1-256)", workers)
}

// Validate timeout
timeout, err := cfg.Section("server").Duration("timeout")
if err != nil || timeout < time.Second || timeout > 5*time.Minute {
return fmt.Errorf("invalid timeout: must be 1s-5m")
}

// Validate limits configuration
maxConns := cfg.Section("limits").MustInt("max_connections", 100)
if maxConns < 1 {
return fmt.Errorf("max_connections must be positive")
}

return nil
}

API Reference

Manager Methods

MethodDescription
New(opts Options) *ManagerCreate new manager
Append(source any, others ...any) errorAppend data source
Batch(fn func(m *Manager) error) errorBatch operations
Reload() errorReload
Section(name string) *SectionGet section
GetSection(name string) (*Section, error)Get section (with error)
HasSection(name string) boolCheck if section exists
NewSection(name string) *SectionCreate new section

Section Methods

MethodDescription
Name() stringGet section name
Parent() (*Section, bool)Get parent section
Key(name string) *KeyGet key
GetKey(name string) (*Key, error)Get key (with error)
HasKey(name string) boolCheck if key exists
NewKey(name, value string) *KeyCreate new key
Keys() []*KeyGet all keys

Value Reading Methods (Section/Key)

MethodDescription
String(name string) stringString value
MustString(name, defaultVal string) stringString with default
Int/Int64/Uint/Uint64(name string) (T, error)Integer value
MustInt/MustInt64/...(name string, defaultVal T) TInteger with default
Float64(name string) (float64, error)Float value
MustFloat64(name, defaultVal float64) float64Float with default
Bool(name string) (bool, error)Boolean value
MustBool(name string, defaultVal bool) boolBoolean with default
Duration(name string) (time.Duration, error)Time duration
Time/TimeFormat(name, format string) (time.Time, error)Time value

List Methods

MethodDescription
Strings(name, delim string) []stringString list
Ints/Int64s/Uints/Uint64s(name, delim string) []TInteger list (invalid as 0)
ValidInts/ValidInt64s/...(name, delim string) []TValid integer list (skip invalid)
StrictInts/StrictInt64s/...(name, delim string) ([]T, error)Strict integer list (error on invalid)
Float64s/Bools/Times(name, delim string) []TOther type lists

Validation Methods

MethodDescription
In(name, defaultVal string, candidates []string) stringCandidate check
InInt/InFloat64/...(name, defaultVal, candidates) TTyped candidate check
RangeInt/RangeFloat64/...(name, defaultVal, min, max) TRange check
Validate(name, fn func(string) string) stringCustom validation

Notes

1. Environment Variable Syntax Differences

Understand the difference between || and ??:

# || Empty fallback: use default when env var doesn't exist or is empty
value1 = ${VAR||default}

# ?? Non-existent fallback: use default only when env var doesn't exist
value2 = ${VAR??default}
// Set empty string
os.Setenv("VAR", "")

cfg.Section("").String("value1") // "default" (|| checks for empty)
cfg.Section("").String("value2") // "" (?? doesn't check for empty)

2. Default Section

Key-value pairs without a section name belong to the default section (empty string name):

# Default section
root = /var/www
debug = true

[app]
name = MyApp
// Access default section
root := cfg.Section("").String("root")
debug := cfg.Section("").MustBool("debug", false)

3. Key Name Case Sensitivity

Depending on Options settings, key names may be converted to lowercase:

// Case-sensitive (default)
cfg := ini.New(ini.Options{})
cfg.Section("app").String("UserName") // Must match exactly

// Case-insensitive
cfg := ini.New(ini.Options{Insensitive: true})
cfg.Section("app").String("username") // Can match UserName, USERNAME, etc.
cfg.Section("app").String("USERNAME") // Also works

4. Multi-Source Merging

Later loaded data sources will override earlier ones with the same key name:

cfg := ini.New(ini.Options{})
cfg.Append("base.ini") // port = 8080
cfg.Append("override.ini") // port = 9000

port := cfg.Section("app").MustInt("port")
// Returns 9000 (override.ini overrode base.ini)

5. Type Conversion Failures

Using Must* series functions, conversion failures will use the default instead of erroring:

// config.ini: port = invalid

port, err := cfg.Section("app").Int("port")
// err != nil, port = 0

port := cfg.Section("app").MustInt("port", 8080)
// port = 8080 (conversion failed, use default)

6. Variable Reference Cycles

Avoid circular references, or they will reach the maximum depth (99 levels):

# Bad example: circular reference
[bad]
a = %(b)s
b = %(a)s

7. Thread Safety

Manager internally uses mutex locks to protect concurrent access. You can customize the Mutex:

cfg := ini.New(ini.Options{
Mutex: &sync.RWMutex{}, // Use read-write lock
})

8. Child Section Inheritance

Child sections inherit keys from parent sections, but do not recursively inherit:

[a]
key1 = value1

[a.b]
key2 = value2

[a.b.c]
key3 = value3
// a.b.c can access keys from a.b, but cannot directly access keys from a
cfg.Section("a.b.c").String("key2") // OK (inherited from a.b)
cfg.Section("a.b.c").String("key1") // Not found (doesn't recursively inherit)

Best Practices

1. Use Environment Variables for Sensitive Configuration

# config.ini - commit to version control
[database]
host = ${DB_HOST||localhost}
port = ${DB_PORT||3306}
name = ${DB_NAME||mydb}
user = ${DB_USER||root}
# Password from environment variable, no default
password = ${DB_PASSWORD}

[api]
# API keys must be provided from environment variables
key = ${API_KEY}
secret = ${API_SECRET}

2. Layered Configuration Files

// Base -> Environment -> Local
cfg := ini.New(ini.Options{Loose: true})
cfg.Batch(func(m *ini.Manager) error {
m.Append("config.ini") // Base configuration
m.Append("config.production.ini") // Production environment
m.Append("config.local.ini") // Local override (not committed)
return nil
})

3. Configuration Validation

func LoadAndValidateConfig() (*ini.Manager, error) {
cfg := ini.New(ini.Options{})
if err := cfg.Append("config.ini"); err != nil {
return nil, err
}

// Validate required configuration
required := map[string][]string{
"database": {"host", "port", "name", "user", "password"},
"redis": {"host", "port"},
}

for section, keys := range required {
for _, key := range keys {
if !cfg.Section(section).HasKey(key) {
return nil, fmt.Errorf("missing required config: %s.%s", section, key)
}
}
}

return cfg, nil
}

4. Use Structured Configuration

type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
}

func (c *Config) Load(cfg *ini.Manager) error {
c.Server.Load(cfg.Section("server"))
c.Database.Load(cfg.Section("database"))
c.Redis.Load(cfg.Section("redis"))
return c.Validate()
}

func (c *Config) Validate() error {
// Centralized validation logic
return nil
}

5. Provide Configuration Template

# config.example.ini - Configuration template
# Copy to config.ini and fill in actual values

[app]
name = MyApp
debug = ${DEBUG||false}
port = ${PORT||8080}

[database]
host = ${DB_HOST||localhost}
port = ${DB_PORT||3306}
name = ${DB_NAME||mydb}
user = ${DB_USER||root}
password = ${DB_PASSWORD} # Required: Database password