https://github.com/ahyalfan/gathuk
Type-safe configuration library for Go (currently .env, .json support, nested structs, env binding)
https://github.com/ahyalfan/gathuk
confg configration environment gathuk golang-library parser
Last synced: 5 months ago
JSON representation
Type-safe configuration library for Go (currently .env, .json support, nested structs, env binding)
- Host: GitHub
- URL: https://github.com/ahyalfan/gathuk
- Owner: ahyalfan
- License: apache-2.0
- Created: 2025-10-31T12:05:01.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2025-11-23T09:24:17.000Z (7 months ago)
- Last Synced: 2025-11-23T11:20:55.418Z (7 months ago)
- Topics: confg, configration, environment, gathuk, golang-library, parser
- Language: Go
- Homepage: https://pkg.go.dev/github.com/ahyalfan/gathuk
- Size: 94.7 KB
- Stars: 5
- Watchers: 0
- Forks: 0
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
# Gathuk


[](https://goreportcard.com/report/github.com/ahyalfan/gathuk)
[](https://godoc.org/github.com/ahyalfan/gathuk)
**Gathuk** is a type-safe, flexible configuration management library for Go that converts configuration files into strongly-typed structs. It supports multiple file formats (currently `.env` `.json`), nested structures, and automatic environment variable binding.
## Features
- 🎯 **Type-Safe**: Uses Go generics for compile-time type safety
- 📁 **Multiple File Formats**: Support for `.env`, `.json` (YAML, TOML coming soon)
- 🔄 **Multiple File Loading**: Load and merge configurations from multiple files
- 🔄 **Automatic Environment Variables**: Automatically bind OS environment variables to struct fields
- 🏗️ **Nested Structures**: Full support for nested struct configurations with custom prefixes
- 🔧 **Flexible Options**: Configure priority between file configs and environment variables
- 💾 **Write Support**: Export configurations back to files
- 🎨 **Custom Codecs**: Extensible codec system for adding new file formats
- 🚀 **Zero Dependencies**: Minimal external dependencies
- ⚡ **High Performance**: Optimized for speed with efficient parsing
## Installation
```bash
go get github.com/ahyalfan/gathuk
```
**Requirements:**
- Go 1.21 or higher (for generics support)
**Verify installation:**
```bash
go list -m github.com/ahyalfan/gathuk
```
## Quick Start
### Basic Usage with .env
```go
package main
import (
"fmt"
"log"
"github.com/ahyalfan/gathuk"
)
type Config struct {
Port int
Host string
}
func main() {
// Create a new Gathuk instance
gt := gathuk.NewGathuk[Config]()
// Load configuration from file
if err := gt.LoadConfigFiles(".env"); err != nil {
log.Fatal(err)
}
// Get the parsed configuration
config := gt.GetConfig()
fmt.Printf("Server: %s:%d\n", config.Host, config.Port)
}
```
**`.env` file:**
```env
PORT=8080
HOST=localhost
```
### Basic Usage with JSON
```go
type Config struct {
Port int `config:"port"`
Host string `config:"host"`
}
func main() {
gt := gathuk.NewGathuk[Config]()
if err := gt.LoadConfigFiles("config.json"); err != nil {
log.Fatal(err)
}
config := gt.GetConfig()
fmt.Printf("Server: %s:%d\n", config.Host, config.Port)
}
```
**`config.json` file:**
```json
{
"port": 8080,
"host": "localhost"
}
```
## Core Concepts
### 1. Generic Type Parameter
Gathuk uses Go generics to provide type-safe configuration loading:
```go
// Concrete struct type (RECOMMENDED)
gt := gathuk.NewGathuk[Config]()
// Generic any type (LIMITED - see warnings)
gt := gathuk.NewGathuk[any]()
// Map type (LIMITED - see warnings)
gt := gathuk.NewGathuk[map[string]any]()
```
**Always prefer concrete struct types for:**
- ✅ Type safety at compile time
- ✅ Proper merging when loading multiple files
- ✅ Better IDE support and autocomplete
- ✅ Self-documenting code
### 2. Configuration Loading Flow
```
Config Files → Tokenize → Parse → Decode → Struct
↓
Environment Variables
↓
Merge & Apply Options
↓
Final Config Struct
```
### 3. Field Name Mapping
Gathuk automatically converts field names to appropriate conventions:
| Go Field Name | .env Format | JSON Format |
| ------------- | ---------------- | ---------------- |
| `Port` | `PORT` | `port` |
| `ServerPort` | `SERVER_PORT` | `server_port` |
| `DatabaseURL` | `DATABASE_U_R_L` | `database_u_r_l` |
| `APIKey` | `A_P_I_KEY` | `a_p_i_key` |
**Override with tags:**
```go
type Config struct {
APIKey string `config:"api_key"` // → API_KEY or api_key
}
```
## Supported Formats
| Format | Extension | Status | Tag Convention |
| --------------------- | --------------- | -------------- | ---------------- |
| Environment Variables | `.env` | ✅ Stable | UPPER_SNAKE_CASE |
| JSON | `.json` | ✅ Stable | lower_snake_case |
| YAML | `.yaml`, `.yml` | 🚧 Coming Soon | lower_snake_case |
| TOML | `.toml` | 🚧 Coming Soon | lower_snake_case |
### Format-Specific Behavior
#### .env Format
**Features:**
- Simple key-value pairs: `KEY=value`
- Comments start with `#`
- Keys automatically converted to UPPER_SNAKE_CASE
- No quotes needed for string values
- Inline comments supported: `PORT=8080 # server port`
**Example:**
```env
# Server Configuration
SERVER_PORT=8080
SERVER_HOST=localhost
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
# Feature Flags
DEBUG=true
ENABLE_LOGGING=true
```
**Supported Types:**
- `string`: Direct text
- `int`, `int64`: Integers
- `float64`: Floating-point numbers
- `bool`: `true` or `false`
#### JSON Format
**Features:**
- Full JSON specification compliance
- Nested objects and arrays
- Keys use lower_snake_case by default
- Type-safe parsing
- Pretty-print support for writing
**Example:**
```json
{
"server": {
"port": 8080,
"host": "localhost",
"timeout": 30
},
"database": {
"host": "localhost",
"port": 5432,
"credentials": {
"username": "admin",
"password": "secret"
}
},
"features": {
"debug": true,
"cache_enabled": true
}
}
```
**Supported Types:**
- All primitive types (string, number, boolean, null)
- Objects (nested structs)
- Arrays (slices)
- Mixed arrays with `[]interface{}`
## Struct Tags
Gathuk supports two main struct tags for customization:
### `config` Tag
Maps struct fields to specific configuration keys:
```go
type Config struct {
// .env: SERVER_PORT | JSON: server_port
Port int `config:"server_port"`
// .env: API_KEY | JSON: api_key
APIKey string `config:"api_key"`
}
```
### `nested` Tag
Defines prefix for nested structures:
```go
type Config struct {
// All Database fields will have DB_ prefix in .env
// In JSON: nested under "db" object
Database Database `config:"db"`
// or
// Database Database `config:"db"`
}
type Database struct {
Host string // .env: DB_HOST | JSON: db.host
Port int // .env: DB_PORT | JSON: db.port
}
```
**Example `.env`:**
```env
DB_HOST=localhost
DB_PORT=5432
```
**Example JSON:**
```json
{
"db": {
"host": "localhost",
"port": 5432
}
}
```
### Ignoring Fields
Use `-` to exclude fields from configuration:
```go
type Config struct {
Internal string `config:"-"` // Will be ignored
}
```
## Configuration Options
### Decode Options
```go
gt := gathuk.NewGathuk[Config]()
// Enable automatic environment variable binding
gt.GlobalDecodeOpt.AutomaticEnv = true
// Prefer file values over environment variables
gt.GlobalDecodeOpt.PreferFileOverEnv = true
// Persist decoded values to OS environment
gt.GlobalDecodeOpt.PersistToOSEnv = true
err := gt.LoadConfigFiles("config.env")
```
### Option Behavior
| Option | Description |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `AutomaticEnv` | When `true`, automatically reads from OS environment variables |
| `PreferFileOverEnv` | When `true`, prioritizes file config over environment variables (requires `AutomaticEnv`) |
| `PersistToOSEnv` | When `true`, saves decoded values to OS environment variables |
### Priority Examples
**Scenario 1: File Only (Default)**
```go
gt := gathuk.NewGathuk[Config]()
// Only reads from config.env
err := gt.LoadConfigFiles("config.env")
```
**Scenario 2: Environment Override**
```go
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
// Environment variables override file values
err := gt.LoadConfigFiles("config.env")
```
**Scenario 3: File Override**
```go
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
gt.GlobalDecodeOpt.PreferFileOverEnv = true
// File values override environment variables
err := gt.LoadConfigFiles("config.env")
```
## Multiple File Loading
Load and merge configurations from multiple files:
```go
gt := gathuk.NewGathuk[Config]()
// Method 1: Load multiple files at once
err := gt.LoadConfigFiles("base.env", "dev.env", "local.env")
// Method 2: Set base files, then load additional files
gt.SetConfigFiles("base.env", "defaults.env")
err := gt.LoadConfigFiles("override.env")
// Method 3: Mix different formats
err := gt.LoadConfigFiles("base.json", "override.env")
```
### Merge Behavior
**Files are processed sequentially:**
1. First file loaded → Initial config
2. Second file loaded → Merged with first
3. Third file loaded → Merged with result of 1+2
4. Continue...
**Merge rules:**
- ✅ Non-zero values from later files **override** earlier files
- ❌ Zero values from later files **do NOT override** earlier files
- ✅ New fields from later files **are added**
- ✅ Nested structs **merge recursively**
### Example: Multi-Environment Setup
```go
// Load base config + environment-specific config
env := os.Getenv("APP_ENV") // "development", "staging", "production"
if env == "" {
env = "development"
}
gt := gathuk.NewGathuk[Config]()
gt.SetConfigFiles("config/base.json")
err := gt.LoadConfigFiles(fmt.Sprintf("config/%s.json", env))
```
### Zero Value Behavior
**IMPORTANT:** Zero values are NOT merged to prevent accidental clearing:
```go
// base.env
PORT=8080
HOST=localhost
MAX_CONNECTIONS=100
// override.env
PORT=0 # Zero value - IGNORED
HOST= # Empty string - IGNORED
MAX_CONNECTIONS=50 # Non-zero - USED
```
```go
gt := gathuk.NewGathuk[Config]()
err := gt.LoadConfigFiles("base.env", "override.env")
config := gt.GetConfig()
// Result:
// Port: 8080 (NOT overridden by 0)
// Host: "localhost" (NOT overridden by "")
// MaxConnections: 50 (overridden by non-zero)
```
**Rationale:** This prevents accidentally clearing important configuration values with empty or zero values in override files.
### Environment-Specific Loading
```go
func LoadConfig() (*Config, error) {
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
files := []string{
"config/base.env", // Always loaded
fmt.Sprintf("config/%s.env", env), // Environment-specific
}
// Add local overrides if exists
localFile := "config/local.env"
if _, err := os.Stat(localFile); err == nil {
files = append(files, localFile)
}
gt := gathuk.NewGathuk[Config]()
if err := gt.LoadConfigFiles(files...); err != nil {
return nil, err
}
return &config, nil
}
```
**Directory structure:**
```
config/
├── base.env # Common settings
├── development.env # Dev overrides
├── staging.env # Staging overrides
├── production.env # Production overrides
└── local.env # Local dev (gitignored)
```
### Priority Examples
#### Example 1: Environment Only
```go
os.Setenv("PORT", "9000")
os.Setenv("HOST", "0.0.0.0")
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
// No files - only environment
err := gt.LoadConfigFiles()
config := gt.GetConfig()
// Port: 9000, Host: "0.0.0.0"
```
#### Example 2: File + Environment (Env Wins)
```env
# config.env
PORT=8080
HOST=localhost
```
```go
os.Setenv("PORT", "9000") // This will win
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
err := gt.LoadConfigFiles("config.env")
config := gt.GetConfig()
// Port: 9000 (from env), Host: "localhost" (from file)
```
#### Example 3: File + Environment (File Wins)
```go
os.Setenv("PORT", "9000") // This will be ignored
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
gt.GlobalDecodeOpt.PreferFileOverEnv = true
err := gt.LoadConfigFiles("config.env")
config := gt.GetConfig()
// Port: 8080 (from file), Host: "localhost" (from file)
```
#### Example 4: Partial Environment
```env
# config.env
PORT=8080
HOST=localhost
```
```go
os.Setenv("DEBUG", "true") // Additional env var
os.Setenv("LOG_LEVEL", "info") // Additional env var
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
err := gt.LoadConfigFiles("config.env")
config := gt.GetConfig()
// Port: 8080 (file), Host: "localhost" (file)
// Debug: true (env), LogLevel: "info" (env)
```
### Docker/Kubernetes Integration
Gathuk works seamlessly with containerized deployments:
```go
type Config struct {
Port int `config:"port"`
DatabaseURL string `config:"database_url"`
RedisURL string `config:"redis_url"`
}
func main() {
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
// In Docker/K8s, all config comes from environment
// Set via docker-compose.yml, Dockerfile ENV, or K8s ConfigMap
err := gt.LoadConfigFiles()
config := gt.GetConfig()
// Ready to use!
}
```
**docker-compose.yml:**
```yaml
services:
app:
environment:
- PORT=8080
- DATABASE_URL=postgres://localhost:5432/db
- REDIS_URL=redis://localhost:6379
```
## Writing Configuration
Export your configuration to files:
```go
config := Config{
Port: 8080,
Host: "localhost",
}
// Write to .env file
err := gt.WriteConfigFile("output.env", 0644, config)
// Write to JSON file
err := gt.WriteConfigFile("output.json", 0644, config)
// Write to io.Writer
var buf bytes.Buffer
err := gt.WriteConfig(&buf, "json", config)
fmt.Println(buf.String())
```
## Advanced Usage
### Custom Codec Registry
Create and register custom codecs for different file formats:
```go
// Create custom codec
type JSONCodec[T any] struct {
option.DefaultCodec[T]
}
func (c *JSONCodec[T]) Decode(buf []byte,val *T) error {
err := json.Unmarshal(buf, val)
return err
}
func (c *JSONCodec[T]) Encode(val T) ([]byte, error) {
return json.Marshal(val)
}
// Register codec
func main() {
registry := gathuk.NewDefaultCodecRegister[Config]()
registry.RegisterCodec("json", &JSONCodec[Config]{})
gt := gathuk.NewGathuk[Config]()
gt.SetCustomCodecRegistry(registry)
err := gt.LoadConfigFiles("config.json")
}
```
### Loading from io.Reader
```go
// From file
file, err := os.Open("config.env")
if err != nil {
log.Fatal(err)
}
defer file.Close()
gt := gathuk.NewGathuk[Config]()
err = gt.LoadConfig(file, "env")
// From HTTP response
resp, err := http.Get("https://api.example.com/config")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
err = gt.LoadConfig(resp.Body, "json")
// From string
configStr := `{"port": 8080, "host": "localhost"}`
reader := strings.NewReader(configStr)
err = gt.LoadConfig(reader, "json")
```
### Format-Specific Options
```go
gt := gathuk.NewGathuk[Config]()
// Set decode options for specific format
envOpt := &option.DecodeOption{
AutomaticEnv: true,
PreferFileOverEnv: true,
}
gt.SetDecodeOption("env", envOpt)
// JSON doesn't need env options
jsonOpt := &option.DecodeOption{
AutomaticEnv: false,
}
gt.SetDecodeOption("json", jsonOpt)
```
### Validation After Loading
```go
type Config struct {
Port int `config:"port"`
Host string `config:"host"`
LogLevel string `config:"log_level"`
}
func (c *Config) Validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("invalid port: %d", c.Port)
}
if c.Host == "" {
return fmt.Errorf("host is required")
}
validLevels := map[string]bool{
"debug": true, "info": true, "warn": true, "error": true,
}
if !validLevels[c.LogLevel] {
return fmt.Errorf("invalid log level: %s", c.LogLevel)
}
return nil
}
func main() {
gt := gathuk.NewGathuk[Config]()
err := gt.LoadConfigFiles("config.env")
if err != nil {
log.Fatal(err)
}
config := gt.GetConfig()
if err := config.Validate(); err != nil {
log.Fatal("Config validation failed:", err)
}
}
```
### Configuration Reloading
```go
type App struct {
config *Config
gt *gathuk.Gathuk[Config]
}
func (app *App) ReloadConfig() error {
if err := app.gt.LoadConfigFiles("config.env"); err != nil {
return err
}
newConfig := app.gt.GetConfig()
// Validate before applying
if err := newConfig.Validate(); err != nil {
return err
}
// Atomic update
app.config = &newConfig
log.Println("Configuration reloaded successfully")
return nil
}
// Reload on signal
func (app *App) WatchConfig() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGHUP)
for {
<-sigChan
if err := app.ReloadConfig(); err != nil {
log.Printf("Failed to reload config: %v", err)
}
}
}
```
## Important Warnings
### ⚠️ Warning 1: Generic Type `any` Behavior
**CRITICAL:** When using `any` or `map[string]any`, multiple file loading does NOT merge:
```go
// ❌ WRONG: Files are NOT merged!
gt := gathuk.NewGathuk[any]()
gt.LoadConfigFiles("base.env", "dev.env")
// Only dev.env values are kept!
// All base.env values are LOST!
// ✅ CORRECT: Use struct type for merging
type Config struct {
Port int
Host string
}
gt := gathuk.NewGathuk[Config]()
gt.LoadConfigFiles("base.env", "dev.env")
// Properly merged!
```
**Why?**
- Struct types: Gathuk knows which fields to merge
- `any`/`map`: Gathuk sees generic map, replaces entirely
- Each load creates new map, discarding previous
**Solutions:**
1. **Use concrete struct types** (recommended)
2. **Load files separately** and merge manually
3. **Load single file** at a time
See [Multiple Files & Merging Documentation](docs/multiple-files.md) for details.
### ⚠️ Warning 2: Zero Values
Zero values from later files do NOT override earlier files:
```go
// base.env
PORT=8080
// override.env
PORT=0 # Will NOT override!
gt.LoadConfigFiles("base.env", "override.env")
// Result: Port = 8080 (not 0)
```
**To force zero values:**
- Load only the override file
- Use a non-zero sentinel value
- Manually set after loading
### ⚠️ Warning 3: Field Names with Acronyms
```go
type Config struct {
APIKey string // → A_P_I_KEY (not API_KEY)
HTTPURL string // → H_T_T_P_U_R_L (not HTTP_URL)
}
// Use tags for better names
type Config struct {
APIKey string `config:"api_key"` // → API_KEY
HTTPURL string `config:"http_url"` // → HTTP_URL
}
```
### ⚠️ Warning 4: Concurrent Access
```go
// ❌ NOT safe for concurrent access during load
gt := gathuk.NewGathuk[Config]()
go gt.LoadConfigFiles("config1.env") // Unsafe!
go gt.LoadConfigFiles("config2.env") // Unsafe!
// ✅ Safe: Load once, read concurrently
gt.LoadConfigFiles("config.env")
config := gt.GetConfig()
go func() { use(config) }() // Safe
go func() { use(config) }() // Safe
```
## Examples
### Example 1: Simple Configuration
```go
// .env file
// PORT=8080
// HOST=localhost
type Config struct {
Port int
Host string
}
gt := gathuk.NewGathuk[Config]()
err := gt.LoadConfigFiles(".env")
// Result: {Port: 8080, Host: "localhost"}
```
### Example 2: With Environment Variable Override
```go
// config.json
// {
// "server": {
// "port": 8080,
// "host": "localhost"
// },
// "database": {
// "host": "db.example.com",
// "port": 5432
// }
// }
type Server struct {
Port int `config:"port"`
Host string `config:"host"`
}
type Database struct {
Host string `config:"host"`
Port int `config:"port"`
}
type Config struct {
Server Server `config:"server"`
Database Database `config:"database"`
}
```
### Example 3: Environment Variable Override
```go
// config.env
// USER=file_user
// PORT=8080
type Config struct {
User string
Port int
Editor string
}
// Set environment variables
os.Setenv("USER", "env_user")
os.Setenv("EDITOR", "nvim")
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
err := gt.LoadConfigFiles("config.env")
// Result: {User: "env_user", Port: 8080, Editor: "nvim"}
// USER from env overrides file, EDITOR only in env, PORT from file
// With PreferFileOverEnv
gt.GlobalDecodeOpt.PreferFileOverEnv = true
err = gt.LoadConfigFiles("config.env")
// Result: {User: "file_user", Port: 8080, Editor: "nvim"}
// USER from file overrides env, EDITOR still from env
```
### Example 4: Multi-Format Configuration
```go
type Config struct {
Server ServerConfig `config:"server"`
Database DatabaseConfig `config:"database"`
}
gt := gathuk.NewGathuk[Config]()
// Load base config from JSON, override with .env
err := gt.LoadConfigFiles("config.json", "override.env")
```
### Example 5: Dynamic Configuration
```go
// Load based on environment
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
configFiles := []string{
"config/base.json",
fmt.Sprintf("config/%s.json", env),
}
// Add local override if exists
if _, err := os.Stat("config/local.json"); err == nil {
configFiles = append(configFiles, "config/local.json")
}
gt := gathuk.NewGathuk[Config]()
err := gt.LoadConfigFiles(configFiles...)
```
## Complete Examples
### Example 1: Web Server Configuration
```go
type Config struct {
Server ServerConfig `config:"server"`
Database DatabaseConfig `config:"db"`
Redis RedisConfig `config:"redis"`
Logging LogConfig `config:"log"`
}
type ServerConfig struct {
Port int `config:"port"`
Host string `config:"host"`
ReadTimeout int `config:"read_timeout"`
WriteTimeout int `config:"write_timeout"`
}
type DatabaseConfig struct {
Host string `config:"host"`
Port int `config:"port"`
User string `config:"user"`
Password string `config:"password"`
Database string `config:"name"`
MaxConns int `config:"max_connections"`
}
type RedisConfig struct {
Host string `config:"host"`
Port int `config:"port"`
Password string `config:"password"`
DB int `config:"db"`
}
type LogConfig struct {
Level string `config:"level"`
Format string `config:"format"`
}
func main() {
// Load configuration
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
files := []string{
"config/base.env",
fmt.Sprintf("config/%s.env", env),
}
if err := gt.LoadConfigFiles(files...); err != nil {
log.Fatal("Failed to load config:", err)
}
config := gt.GetConfig()
// Validate configuration
if config.Server.Port < 1 || config.Server.Port > 65535 {
log.Fatal("Invalid server port")
}
// Start server
addr := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)
log.Printf("Starting server on %s", addr)
// Use configuration
db := connectDatabase(config.Database)
redis := connectRedis(config.Redis)
server := &http.Server{
Addr: addr,
ReadTimeout: time.Duration(config.Server.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(config.Server.WriteTimeout) * time.Second,
}
log.Fatal(server.ListenAndServe())
}
```
**`config/base.env`:**
```env
# Server Configuration
SERVER_PORT=8080
SERVER_HOST=0.0.0.0
SERVER_READ_TIMEOUT=30
SERVER_WRITE_TIMEOUT=30
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_USER=app
DB_PASSWORD=secret
DB_NAME=myapp
DB_MAX_CONNECTIONS=25
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Logging Configuration
LOG_LEVEL=info
LOG_FORMAT=json
```
**`config/development.env`:**
```env
# Override for development
SERVER_PORT=3000
DB_MAX_CONNECTIONS=5
LOG_LEVEL=debug
LOG_FORMAT=text
```
### Example 2: Microservice Configuration
```go
type Config struct {
Service ServiceConfig `config:"service"`
HTTP HTTPConfig `config:"http"`
GRPC GRPCConfig `config:"grpc"`
Observability ObservabilityConfig `config:"obs"`
Dependencies DependenciesConfig `config:"deps"`
}
type ServiceConfig struct {
Name string `config:"name"`
Version string `config:"version"`
Environment string `config:"environment"`
}
type HTTPConfig struct {
Enabled bool `config:"enabled"`
Port int `config:"port"`
Timeout int `config:"timeout"`
}
type GRPCConfig struct {
Enabled bool `config:"enabled"`
Port int `config:"port"`
Timeout int `config:"timeout"`
}
type ObservabilityConfig struct {
Metrics MetricsConfig `config:"metrics"`
Tracing TracingConfig `config:"tracing"`
Logging LoggingConfig `config:"logging"`
}
type MetricsConfig struct {
Enabled bool `config:"enabled"`
Port int `config:"port"`
Path string `config:"path"`
}
type TracingConfig struct {
Enabled bool `config:"enabled"`
Endpoint string `config:"endpoint"`
SampleRate float64 `config:"sample_rate"`
}
type LoggingConfig struct {
Level string `config:"level"`
Format string `config:"format"`
}
type DependenciesConfig struct {
Database DatabaseConfig `config:"db"`
Cache CacheConfig `config:"cache"`
Queue QueueConfig `config:"queue"`
}
type DatabaseConfig struct {
Host string `config:"host"`
Port int `config:"port"`
User string `config:"user"`
Password string `config:"password"`
Database string `config:"name"`
MaxOpenConns int `config:"max_open_conns"`
MaxIdleConns int `config:"max_idle_conns"`
}
type CacheConfig struct {
Host string `config:"host"`
Port int `config:"port"`
Password string `config:"password"`
TTL int `config:"ttl"`
}
type QueueConfig struct {
URL string `config:"url"`
MaxRetries int `config:"max_retries"`
RetryDelay int `config:"retry_delay"`
}
func LoadConfig() (*Config, error) {
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
// Load base + environment-specific config
env := os.Getenv("SERVICE_ENVIRONMENT")
if env == "" {
env = "development"
}
files := []string{
"config/base.env",
fmt.Sprintf("config/%s.env", env),
}
// Add secrets file if exists (for local development)
if _, err := os.Stat("config/secrets.env"); err == nil {
files = append(files, "config/secrets.env")
}
if err := gt.LoadConfigFiles(files...); err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
config := gt.GetConfig()
// Validate
if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
}
return &config, nil
}
func validateConfig(cfg *Config) error {
if cfg.Service.Name == "" {
return fmt.Errorf("service name is required")
}
if !cfg.HTTP.Enabled && !cfg.GRPC.Enabled {
return fmt.Errorf("at least one protocol (HTTP or GRPC) must be enabled")
}
if cfg.HTTP.Enabled && (cfg.HTTP.Port < 1 || cfg.HTTP.Port > 65535) {
return fmt.Errorf("invalid HTTP port: %d", cfg.HTTP.Port)
}
if cfg.GRPC.Enabled && (cfg.GRPC.Port < 1 || cfg.GRPC.Port > 65535) {
return fmt.Errorf("invalid GRPC port: %d", cfg.GRPC.Port)
}
return nil
}
```
### Example 3: CLI Application Configuration
```go
type Config struct {
App AppConfig `config:"app"`
API APIConfig `config:"api"`
Output OutputConfig `config:"output"`
Advanced AdvancedConfig `config:"advanced"`
}
type AppConfig struct {
Name string `config:"name"`
Version string `config:"version"`
Debug bool `config:"debug"`
}
type APIConfig struct {
BaseURL string `config:"base_url"`
Token string `config:"token"`
Timeout int `config:"timeout"`
}
type OutputConfig struct {
Format string `config:"format"` // json, yaml, table
Color bool `config:"color"`
Quiet bool `config:"quiet"`
}
type AdvancedConfig struct {
CacheDir string `config:"cache_dir"`
MaxRetries int `config:"max_retries"`
RetryDelay int `config:"retry_delay"`
}
func main() {
// Parse flags
configFile := flag.String("config", "", "Config file path")
debug := flag.Bool("debug", false, "Enable debug mode")
flag.Parse()
// Load configuration
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
files := []string{}
// Load from default locations
homeDir, _ := os.UserHomeDir()
defaultFiles := []string{
filepath.Join(homeDir, ".myapp", "config.env"),
".myapp.env",
}
for _, f := range defaultFiles {
if _, err := os.Stat(f); err == nil {
files = append(files, f)
}
}
// Load from specified config file
if *configFile != "" {
files = append(files, *configFile)
}
if len(files) > 0 {
if err := gt.LoadConfigFiles(files...); err != nil {
log.Fatal("Failed to load config:", err)
}
}
config := gt.GetConfig()
// Override with flags
if *debug {
config.App.Debug = true
}
// Use configuration
runCLI(config)
}
```
### Example 4: Testing Configuration
```go
func TestConfigLoading(t *testing.T) {
tests := []struct {
name string
envFile string
envVars map[string]string
want Config
wantErr bool
}{
{
name: "basic config",
envFile: "testdata/basic.env",
want: Config{
Port: 8080,
Host: "localhost",
},
wantErr: false,
},
{
name: "with environment override",
envFile: "testdata/basic.env",
envVars: map[string]string{
"PORT": "9000",
},
want: Config{
Port: 9000,
Host: "localhost",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set environment variables
for k, v := range tt.envVars {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
// Load config
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
err := gt.LoadConfigFiles(tt.envFile)
if (err != nil) != tt.wantErr {
t.Errorf("LoadConfigFiles() error = %v, wantErr %v", err, tt.wantErr)
return
}
got := gt.GetConfig()
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetConfig() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfigMerging(t *testing.T) {
// Create temporary files
baseFile := createTempFile(t, `
PORT=8080
HOST=localhost
DEBUG=false
`)
defer os.Remove(baseFile)
devFile := createTempFile(t, `
DEBUG=true
LOG_LEVEL=debug
`)
defer os.Remove(devFile)
// Load and merge
gt := gathuk.NewGathuk[Config]()
err := gt.LoadConfigFiles(baseFile, devFile)
if err != nil {
t.Fatal(err)
}
config := gt.GetConfig()
// Verify merged results
if config.Port != 8080 {
t.Errorf("Port = %d, want 8080", config.Port)
}
if config.Host != "localhost" {
t.Errorf("Host = %s, want localhost", config.Host)
}
if config.Debug != true {
t.Errorf("Debug = %v, want true", config.Debug)
}
if config.LogLevel != "debug" {
t.Errorf("LogLevel = %s, want debug", config.LogLevel)
}
}
func createTempFile(t *testing.T, content string) string {
t.Helper()
file, err := os.CreateTemp("", "config-*.env")
if err != nil {
t.Fatal(err)
}
if _, err := file.WriteString(content); err != nil {
t.Fatal(err)
}
file.Close()
return file.Name()
}
```
### Example 5: Dynamic Configuration Switching
```go
type ConfigManager struct {
configs map[string]*Config
active string
mu sync.RWMutex
}
func NewConfigManager() *ConfigManager {
return &ConfigManager{
configs: make(map[string]*Config),
active: "default",
}
}
func (cm *ConfigManager) LoadProfile(name, file string) error {
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
if err := gt.LoadConfigFiles(file); err != nil {
return fmt.Errorf("failed to load profile %s: %w", name, err)
}
config := gt.GetConfig()
cm.mu.Lock()
cm.configs[name] = &config
cm.mu.Unlock()
return nil
}
func (cm *ConfigManager) SwitchProfile(name string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if _, exists := cm.configs[name]; !exists {
return fmt.Errorf("profile %s not found", name)
}
cm.active = name
log.Printf("Switched to profile: %s", name)
return nil
}
func (cm *ConfigManager) GetConfig() Config {
cm.mu.RLock()
defer cm.mu.RUnlock()
return *cm.configs[cm.active]
}
func main() {
manager := NewConfigManager()
// Load multiple profiles
profiles := map[string]string{
"development": "config/dev.env",
"staging": "config/staging.env",
"production": "config/prod.env",
}
for name, file := range profiles {
if err := manager.LoadProfile(name, file); err != nil {
log.Printf("Warning: %v", err)
}
}
// Use active profile
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
if err := manager.SwitchProfile(env); err != nil {
log.Fatal(err)
}
config := manager.GetConfig()
log.Printf("Running with config: %+v", config)
}
```
## Performance
Gathuk is optimized for performance with efficient parsing and minimal allocations.
### Benchmark Results
```
goos: linux
goarch: amd64
pkg: github.com/ahyalfan/gathuk
cpu: AMD Ryzen 5 6600H with Radeon Graphics
BenchmarkGathuk/Simple_Load-12 116638 10046 ns/op 3240 B/op 50 allocs/op
BenchmarkGathuk/Nested_Struct-12 113784 10685 ns/op 3432 B/op 62 allocs/op
BenchmarkGathuk/Multiple_Files-12 57288 20465 ns/op 6152 B/op 102 allocs/op
```
### Run Benchmarks
```bash
# Run all benchmarks
go test -bench=. -benchmem
# Run specific benchmark
go test -bench=BenchmarkGathuk/Simple -benchmem
# With CPU profiling
go test -bench=. -benchmem -cpuprofile=cpu.prof
# With memory profiling
go test -bench=. -benchmem -memprofile=mem.prof
```
**What this means:**
- **Simple Load**: ~10 microseconds per operation
- **Nested Struct**: ~11 microseconds per operation
- **Multiple Files**: ~20 microseconds per operation (loading 2 files)
**Memory efficiency:**
- Simple config: ~3.2 KB per load
- Nested struct: ~3.4 KB per load
- Multiple files: ~6.1 KB per load
### Performance Tips
1. **Reuse Gathuk instances when possible**
```go
// ✅ Good: Reuse instance
gt := gathuk.NewGathuk[Config]()
for _, file := range files {
gt.LoadConfigFiles(file)
}
// ❌ Avoid: Creating new instance each time
for _, file := range files {
gt := gathuk.NewGathuk[Config]() // Unnecessary allocation
gt.LoadConfigFiles(file)
}
```
1. **Load once, use many times**
```go
// ✅ Good: Load once at startup
var GlobalConfig Config
func init() {
gt := gathuk.NewGathuk[Config]()
gt.LoadConfigFiles("config.env")
GlobalConfig = gt.GetConfig()
}
func handler1() {
// Use GlobalConfig
}
func handler2() {
// Use GlobalConfig
}
```
1. **Use concrete struct types**
```go
// ✅ Good: Concrete type (faster)
gt := gathuk.NewGathuk[Config]()
// ❌ Slower: Generic any type
gt := gathuk.NewGathuk[any]()
```
## Best Practices
### 1. Always Use Struct Types for Multiple Files
```go
// ✅ DO: Use concrete struct for merging
type Config struct {
Port int
Host string
}
gt := gathuk.NewGathuk[Config]()
gt.LoadConfigFiles("base.env", "dev.env")
// ❌ DON'T: Use any with multiple files
gt := gathuk.NewGathuk[any]()
gt.LoadConfigFiles("base.env", "dev.env") // Only last file kept!
```
### 2. Organize Config Files by Environment
```
config/
├── base.env # Common settings (all environments)
├── development.env # Dev-specific overrides
├── staging.env # Staging-specific overrides
├── production.env # Production-specific overrides
├── secrets.env # Secrets (gitignored, optional)
└── local.env # Local dev overrides (gitignored)
```
### 3. Validate Configuration After Loading
```go
type Config struct {
Port int
Host string
LogLevel string
}
func (c *Config) Validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("invalid port: %d", c.Port)
}
if c.Host == "" {
return fmt.Errorf("host is required")
}
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
if !validLevels[c.LogLevel] {
return fmt.Errorf("invalid log level: %s", c.LogLevel)
}
return nil
}
func main() {
gt := gathuk.NewGathuk[Config]()
gt.LoadConfigFiles("config.env")
config := gt.GetConfig()
if err := config.Validate(); err != nil {
log.Fatal("Configuration error:", err)
}
}
```
### 4. Use Struct Tags Consistently
```go
// ✅ Good: Explicit and consistent
type Config struct {
ServerPort int `config:"server_port"`
DBHost string `config:"db_host"`
APIKey string `config:"api_key"`
}
// ❌ Avoid: Mixing conventions
type Config struct {
ServerPort int // Auto-mapped
DBHost string `config:"database_host"` // Custom
APIKey string // Auto-mapped (becomes A_P_I_KEY!)
}
```
### 5. Document Your Configuration
```go
type Config struct {
// Server listening port (default: 8080, range: 1-65535)
Port int `config:"port"`
// Server bind address (default: "localhost")
// Use "0.0.0.0" to listen on all interfaces
Host string `config:"host"`
// Maximum number of database connections (default: 100)
MaxConnections int `config:"max_connections"`
// Enable debug logging (default: false)
// WARNING: Debug logging may expose sensitive information
Debug bool `config:"debug"`
}
```
### 6. Use Environment Variables for Secrets
```go
// ❌ Don't: Store secrets in config files
// config.env (committed to git)
DATABASE_PASSWORD=mysecret123
// ✅ Do: Use environment variables for secrets
// config.env (committed to git)
DATABASE_HOST=localhost
DATABASE_PORT=5432
// Set secrets via environment
export DATABASE_PASSWORD=mysecret123
// Or use separate secrets file (gitignored)
// config/secrets.env (gitignored)
DATABASE_PASSWORD=mysecret123
```
### 7. Provide Sensible Defaults
```go
type Config struct {
Port int `config:"port"`
Host string `config:"host"`
ReadTimeout int `config:"read_timeout"`
WriteTimeout int `config:"write_timeout"`
}
func LoadConfigWithDefaults() (*Config, error) {
// Set defaults
config := Config{
Port: 8080,
Host: "localhost",
ReadTimeout: 30,
WriteTimeout: 30,
}
// Override with file values
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
// LoadConfigFiles will only override non-zero values
if err := gt.LoadConfigFiles("config.env"); err != nil {
// If config file doesn't exist, use defaults
if !os.IsNotExist(err) {
return nil, err
}
} else {
config = gt.GetConfig()
}
return &config, nil
}
```
### 8. Test Configuration Loading
```go
func TestConfigLoading(t *testing.T) {
// Create test config file
content := `
PORT=8080
HOST=localhost
DEBUG=true
`
tmpfile, err := os.CreateTemp("", "config-*.env")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.WriteString(content); err != nil {
t.Fatal(err)
}
tmpfile.Close()
// Load config
gt := gathuk.NewGathuk[Config]()
if err := gt.LoadConfigFiles(tmpfile.Name()); err != nil {
t.Fatal(err)
}
config := gt.GetConfig()
// Assert values
if config.Port != 8080 {
t.Errorf("Port = %d, want 8080", config.Port)
}
if config.Host != "localhost" {
t.Errorf("Host = %s, want localhost", config.Host)
}
if config.Debug != true {
t.Errorf("Debug = %v, want true", config.Debug)
}
}
```
### 9. Handle Missing Files Gracefully
```go
func LoadConfig(files ...string) (*Config, error) {
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
// Filter existing files
existingFiles := []string{}
for _, file := range files {
if _, err := os.Stat(file); err == nil {
existingFiles = append(existingFiles, file)
} else {
log.Printf("Config file not found (skipping): %s", file)
}
}
if len(existingFiles) == 0 {
return nil, fmt.Errorf("no config files found")
}
if err := gt.LoadConfigFiles(existingFiles...); err != nil {
return nil, err
}
config := gt.GetConfig()
return &config, nil
}
```
### 10. Use Feature Flags Pattern
```go
type Config struct {
Features FeatureFlags `config:"feature"`
}
type FeatureFlags struct {
EnableNewUI bool `config:"new_ui"`
EnableBetaAPI bool `config:"beta_api"`
EnableCaching bool `config:"caching"`
}
// Load base config with all features disabled
// Then override based on environment
// base.env
FEATURE_NEW_UI=false
FEATURE_BETA_API=false
FEATURE_CACHING=true
// production.env
FEATURE_CACHING=true
// development.env
FEATURE_NEW_UI=true
FEATURE_BETA_API=true
FEATURE_CACHING=false
```
## Migration Guide
### From Viper
**Before (Viper):**
```go
import "github.com/spf13/viper"
viper.SetConfigName("config")
viper.SetConfigType("json")
viper.AddConfigPath(".")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
log.Fatal(err)
}
port := viper.GetInt("server.port")
host := viper.GetString("server.host")
```
**After (Gathuk):**
```go
import "github.com/ahyalfan/gathuk"
type Config struct {
Server struct {
Port int `config:"port"`
Host string `config:"host"`
} `config:"server"`
}
gt := gathuk.NewGathuk[Config]()
gt.GlobalDecodeOpt.AutomaticEnv = true
if err := gt.LoadConfigFiles("config.json"); err != nil {
log.Fatal(err)
}
config := gt.GetConfig()
port := config.Server.Port
host := config.Server.Host
```
**Benefits:**
- ✅ Type-safe access (no Get\* methods)
- ✅ Compile-time checking
- ✅ Better IDE support
- ✅ No string keys to remember
### From godotenv
**Before (godotenv):**
```go
import "github.com/joho/godotenv"
if err := godotenv.Load(); err != nil {
log.Fatal(err)
}
port, _ := strconv.Atoi(os.Getenv("PORT"))
host := os.Getenv("HOST")
debug := os.Getenv("DEBUG") == "true"
```
**After (Gathuk):**
```go
import "github.com/ahyalfan/gathuk"
type Config struct {
Port int
Host string
Debug bool
}
gt := gathuk.NewGathuk[Config]()
if err := gt.LoadConfigFiles(".env"); err != nil {
log.Fatal(err)
}
config := gt.GetConfig()
port := config.Port // Automatically converted to int
host := config.Host
debug := config.Debug // Automatically converted to bool
```
**Benefits:**
- ✅ Automatic type conversion
- ✅ No manual parsing
- ✅ Type-safe struct
- ✅ Less boilerplate
### From encoding/json
**Before (encoding/json):**
```go
import "encoding/json"
file, err := os.Open("config.json")
if err != nil {
log.Fatal(err)
}
defer file.Close()
var config Config
decoder := json.NewDecoder(file)
if err := decoder.Decode(&config); err != nil {
log.Fatal(err)
}
```
**After (Gathuk):**
```go
import "github.com/ahyalfan/gathuk"
gt := gathuk.NewGathuk[Config]()
if err := gt.LoadConfigFiles("config.json"); err != nil {
log.Fatal(err)
}
config := gt.GetConfig()
```
**Benefits:**
- ✅ Environment variable support
- ✅ Multiple file merging
- ✅ Format-agnostic (same code for .env, JSON, etc.)
- ✅ Less boilerplate
## API Reference
### Core Functions
#### `NewGathuk[T any]() *Gathuk[T]`
Creates a new Gathuk instance with default configuration.
#### `LoadConfigFiles(srcFiles ...string) error`
Loads and merges configurations from one or more files.
#### `LoadConfig(src io.Reader, format string) error`
Loads configuration from an io.Reader with specified format.
#### `GetConfig() T`
Returns the parsed configuration struct.
#### `WriteConfigFile(dst string, mode fs.FileMode, config T) error`
Writes configuration to a file.
#### `WriteConfig(out io.Writer, format string, config T) error`
Writes configuration to an io.Writer.
#### `SetConfigFiles(srcFiles ...string)`
Sets base configuration files without loading them.
#### `SetCustomCodecRegistry(c option.CodecRegistry[T]) *Gathuk[T]`
Sets a custom codec registry for handling different file formats.
#### `SetDecodeOption(format string, opt *option.DecodeOption)`
Sets decode options for a specific format.
#### `SetEncodeOption(format string, opt *option.EncodeOption)`
Sets encode options for a specific format.
### For complete API documentation, see [GoDoc](https://godoc.org/github.com/ahyalfan/gathuk)
## FAQ
**Q: Can I use multiple formats simultaneously?**
A: Yes! You can load different formats in sequence: `gt.LoadConfigFiles("base.json", "override.env")`
**Q: How do I handle missing configuration files?**
A: Check for `os.IsNotExist(err)` and provide defaults or use fallback files.
**Q: Can I reload configuration at runtime?**
A: Yes, call `LoadConfigFiles()` again. Values will be merged with existing configuration.
**Q: Does it support configuration validation?**
A: Validate after loading using your own validation logic or libraries like `go-playground/validator`.
**Q: How do I set default values?**
A: Initialize your struct with defaults before loading: `config := Config{Port: 8080}`
**Q: Can I use with Docker/Kubernetes?**
A: Yes! Use `AutomaticEnv` to read from environment variables set by orchestration tools.
**Q: Is it thread-safe?**
A: Reading config after loading is thread-safe. Loading config should be done during initialization.
## Roadmap
- [x] .env format support
- [x] JSON format support
- [x] Environment variable binding
- [x] Multiple file merging
- [x] Nested structure support
- [x] Write support
- [ ] YAML format support
- [ ] TOML format support
- [ ] Configuration validation
- [ ] Hot reload support
- [ ] Configuration encryption
- [ ] Remote config sources (etcd, consul)
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
### Development Setup
```bash
# Clone the repository
git clone https://github.com/ahyalfan/gathuk.git
cd gathuk
# Run tests
go test ./...
# Run benchmarks
go test -bench=. -benchmem
# Run with coverage
go test -cover ./...
# Generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
```
### Contribution Guidelines
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Write tests for your changes
4. Ensure all tests pass (`go test ./...`)
5. Run `go fmt ./...` to format code
6. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
7. Push to the branch (`git push origin feature/AmazingFeature`)
8. Open a Pull Request
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
## Author
- [@ahyalfan](https://github.com/ahyalfan)
## Acknowledgments
- Inspired by [Viper](https://github.com/spf13/viper) and [godotenv](https://github.com/joho/godotenv)
- Thanks to all contributors
## Support
If you find this project helpful, please give it a ⭐️!
For issues and questions, please use the [GitHub issue tracker](https://github.com/ahyalfan/gathuk/issues).