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 sliceio.Reader: Readerio.ReadCloser: Closable readerDataSource: Custom data source interfacefunc() (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
| Method | Description |
|---|---|
New(opts Options) *Manager | Create new manager |
Append(source any, others ...any) error | Append data source |
Batch(fn func(m *Manager) error) error | Batch operations |
Reload() error | Reload |
Section(name string) *Section | Get section |
GetSection(name string) (*Section, error) | Get section (with error) |
HasSection(name string) bool | Check if section exists |
NewSection(name string) *Section | Create new section |
Section Methods
| Method | Description |
|---|---|
Name() string | Get section name |
Parent() (*Section, bool) | Get parent section |
Key(name string) *Key | Get key |
GetKey(name string) (*Key, error) | Get key (with error) |
HasKey(name string) bool | Check if key exists |
NewKey(name, value string) *Key | Create new key |
Keys() []*Key | Get all keys |
Value Reading Methods (Section/Key)
| Method | Description |
|---|---|
String(name string) string | String value |
MustString(name, defaultVal string) string | String with default |
Int/Int64/Uint/Uint64(name string) (T, error) | Integer value |
MustInt/MustInt64/...(name string, defaultVal T) T | Integer with default |
Float64(name string) (float64, error) | Float value |
MustFloat64(name, defaultVal float64) float64 | Float with default |
Bool(name string) (bool, error) | Boolean value |
MustBool(name string, defaultVal bool) bool | Boolean with default |
Duration(name string) (time.Duration, error) | Time duration |
Time/TimeFormat(name, format string) (time.Time, error) | Time value |
List Methods
| Method | Description |
|---|---|
Strings(name, delim string) []string | String list |
Ints/Int64s/Uints/Uint64s(name, delim string) []T | Integer list (invalid as 0) |
ValidInts/ValidInt64s/...(name, delim string) []T | Valid integer list (skip invalid) |
StrictInts/StrictInt64s/...(name, delim string) ([]T, error) | Strict integer list (error on invalid) |
Float64s/Bools/Times(name, delim string) []T | Other type lists |
Validation Methods
| Method | Description |
|---|---|
In(name, defaultVal string, candidates []string) string | Candidate check |
InInt/InFloat64/...(name, defaultVal, candidates) T | Typed candidate check |
RangeInt/RangeFloat64/...(name, defaultVal, min, max) T | Range check |
Validate(name, fn func(string) string) string | Custom 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