13 KiB
name, description
| name | description |
|---|---|
| golang-flexible-config-skill | This skill defines how an AI agent should implement configuration loading in any Go application. |
Skill: Go Configuration Pipeline (Launch Arg → Env → Config File → Defaults)
Description: This skill defines how an AI agent should implement configuration loading in any Go application. The configuration must be loaded in the following order of priority (each source overrides the previous):
- Launch arguments (command-line flags) – must be handled via Cobra.
- Environment variables (uppercase, dots/dashes replaced by underscores).
- Configuration file (YAML/JSON/TOML).
- Hardcoded defaults.
The skill enforces consistent logging of the config file location (found or not), a well-defined search path list, and a generate-config subcommand that produces a commented example config in either English or Russian.
All subcommands and flag definitions must use the Cobra library (github.com/spf13/cobra).
Env Variable Rules
- All environment variables must use UPPER_CASE.
- Dots (
.) and dashes (-) in config property names are replaced with underscores (_). - Example: property
db.pathmaps to envAPP_DB_PATH,log-levelmaps toAPP_LOG_LEVEL. - Use
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")).
Config File Search Order
The config file is searched in the following order (first found wins). The agent must log the exact path where the file was found. If no file is found, log "Configuration file not found" and log the list of all checked paths so the user knows where to place one.
- Environment variable
APP_CONFIG_LOCATION(or custom prefix) – absolute path. - Working directory –
./config.yaml(or other extensions). - User config directory –
~/.config/{app-name}/config.yaml. - Binary directory – directory containing the executable.
- System Config default - directory such as /etc/* on unix systems
Subcommand: generate-config (Cobra based)
The application must support a subcommand generate-config that writes an example config file with detailed comments.
- Flag
--lang(default"en", possible values"en"and"ru").en: comments in English.ru: comments in Russian.
- Flag
--output(optional; if not provided, prints to stdout; if given, writes to that file).
The generated file must include every configuration parameter with a meaningful comment describing its purpose and type.
Example Implementation (Template for AI Agent – Cobra version)
Below is a complete, production‑ready example. The agent should use this pattern or a very similar one.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Config structure with mapstructure tags
type Config struct {
Port int `mapstructure:"port"`
DBPath string `mapstructure:"db_path"`
LogLevel string `mapstructure:"log_level"`
}
func main() {
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "MyApp configuration loader",
Long: `Loads configuration from defaults, file, env, and CLI flags (in order of priority).`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
_ = cfg // use cfg in your application
return nil
},
}
// Define main flags
rootCmd.Flags().Int("port", 0, "listen port (overrides env/file)")
rootCmd.Flags().String("db-path", "", "path to database")
rootCmd.Flags().String("log-level", "", "log level (debug, info, warn, error)")
// Bind Viper to flags
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
viper.BindPFlag("db_path", rootCmd.Flags().Lookup("db-path"))
viper.BindPFlag("log_level", rootCmd.Flags().Lookup("log-level"))
// Subcommand: generate-config
var generateCmd = &cobra.Command{
Use: "generate-config",
Short: "Generate an example config file",
Run: func(cmd *cobra.Command, args []string) {
lang, _ := cmd.Flags().GetString("lang")
output, _ := cmd.Flags().GetString("output")
generateConfig(lang, output)
},
}
generateCmd.Flags().String("lang", "en", "Language for comments: en or ru")
generateCmd.Flags().String("output", "", "Output file path (if empty, prints to stdout)")
rootCmd.AddCommand(generateCmd)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
// loadConfig implements the ordered configuration loading
func loadConfig() (*Config, error) {
// 1. Defaults (lowest priority)
viper.SetDefault("port", 8080)
viper.SetDefault("db_path", "./data.db")
viper.SetDefault("log_level", "info")
// 2. Config file search with logging
viper.SetConfigName("config")
viper.SetConfigType("yaml") // can also support json, toml, etc.
// Collect all search paths for logging
searchPaths := []string{}
// a) Environment variable APP_CONFIG_LOCATION
if envLoc := os.Getenv("APP_CONFIG_LOCATION"); envLoc != "" {
viper.SetConfigFile(envLoc)
searchPaths = append(searchPaths, fmt.Sprintf("ENV: APP_CONFIG_LOCATION = %s", envLoc))
if err := viper.ReadInConfig(); err == nil {
log.Printf("Config file found: %s (from APP_CONFIG_LOCATION)", viper.ConfigFileUsed())
} else {
return nil, fmt.Errorf("config file specified in APP_CONFIG_LOCATION not found: %s", envLoc)
}
} else {
// b) Working directory
wd, _ := os.Getwd()
searchPaths = append(searchPaths, fmt.Sprintf("Working directory: %s", filepath.Join(wd, "config.yaml")))
viper.AddConfigPath(".")
// c) ~/.config/{app-name}/
homeDir, _ := os.UserHomeDir()
appName := "myapp" // replace with your application name
userConfigPath := filepath.Join(homeDir, ".config", appName)
searchPaths = append(searchPaths, fmt.Sprintf("User config: %s", filepath.Join(userConfigPath, "config.yaml")))
viper.AddConfigPath(userConfigPath)
// d) Binary directory
exePath, _ := os.Executable()
exeDir := filepath.Dir(exePath)
searchPaths = append(searchPaths, fmt.Sprintf("Binary directory: %s", filepath.Join(exeDir, "config.yaml")))
viper.AddConfigPath(exeDir)
// e) /etc/{app-name}/
etcPath := filepath.Join("/etc", appName)
searchPaths = append(searchPaths, fmt.Sprintf("System config: %s", filepath.Join(etcPath, "config.yaml")))
viper.AddConfigPath(etcPath)
// Try to read config
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Println("Configuration file not found. Using defaults and env/args.")
log.Println("Search paths checked:")
for _, p := range searchPaths {
log.Println(" ", p)
}
} else {
return nil, fmt.Errorf("error reading config: %w", err)
}
} else {
log.Printf("Config file found: %s", viper.ConfigFileUsed())
}
}
// 3. Environment variables
viper.SetEnvPrefix("APP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
// 4. Command-line flags – already bound via Cobra, Viper will read them
// No additional code needed, flags are already processed by Cobra
// 5. Decode into Config struct
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("config unmarshal failed: %w", err)
}
return &cfg, nil
}
// generateConfig writes an example config file with English or Russian comments
func generateConfig(lang, output string) {
var commentLines []string
switch lang {
case "ru":
commentLines = []string{
"# Конфигурация приложения MyApp",
"# Все значения могут быть переопределены через переменные окружения (префикс APP_)",
"# или аргументы командной строки.",
"",
"port: 8080 # Порт, на котором будет слушать HTTP-сервер",
"db_path: \"./data.db\" # Путь к файлу базы данных SQLite",
"log_level: \"info\" # Уровень логирования: debug, info, warn, error",
}
default: // en
commentLines = []string{
"# MyApp configuration file",
"# All values can be overridden by environment variables (prefix APP_)",
"# or command-line flags.",
"",
"port: 8080 # HTTP server listening port",
"db_path: \"./data.db\" # Path to SQLite database file",
"log_level: \"info\" # Log level: debug, info, warn, error",
}
}
content := strings.Join(commentLines, "\n") + "\n"
if output != "" {
if err := os.WriteFile(output, []byte(content), 0644); err != nil {
log.Fatalf("Failed to write example config: %v", err)
}
log.Printf("Example config written to %s", output)
} else {
fmt.Print(content)
}
}
Agent Instructions
- Use the code above as a baseline.
- All subcommands and CLI flags must be implemented via Cobra (
github.com/spf13/cobra). - Always include logging of config file location (found or not) and all checked search paths.
- The
generate-configsubcommand must accept--lang(default"en", can be"ru") and--output. - Comments inside the generated config must be meaningful and match the selected language.
- The config search order and env variable rules are mandatory.
- If the agent uses a different configuration library (e.g.,
envconfig,cleanenv), the same semantics must be maintained, but CLI handling must still be through Cobra. - Never set
SilenceUsage: trueon the root command or any subcommand. Incorrect flags or invalid subcommands must result in an explicit error message and the usage output. Cobra’s default behavior already provides this; leaveSilenceUsageandSilenceErrorsat their default (false). Do not suppress automatic usage printing on parse errors.
Self‑Check: Adding a New Config Property
When you introduce a new configuration property to the application (e.g. max_connections), verify each of the following points. Tick all boxes before considering the change complete.
- 1. Default value – A sensible hardcoded default is set in
viper.SetDefault(...)(or equivalent). - 2. Struct field – The property exists as a field in the
Configstruct with a correctmapstructuretag. - 3. Env mapping – The corresponding environment variable is automatically picked up. Ensure the env name follows the pattern: prefix + uppercase + underscores (dots/dashes replaced).
Example: formax_connectionsyou should be able to setAPP_MAX_CONNECTIONS. - 4. Command‑line flag (Cobra) – A flag is defined on the appropriate Cobra command (root or sub) and bound via
viper.BindPFlag(...).
Example flag name:--max-connections(dashes). The binding uses the struct keymax_connections. - 5. Config file key – The property is documented as a key in the example config file (YAML key matches the struct field name).
- 6. Comments in
generate-config– The example config output includes a meaningful comment for the new property in both English and Russian (if the--lang rucase is implemented). The comment explains the unit, default, and allowed values if applicable. - 7. Logging – No additional logging is required for a single property change; the existing logging of the config file location is sufficient.
- 8. Consistency check – Verify that the same property name is used consistently across: struct tag, env prefix replacement, flag binding, and config file key.
Run through this checklist every time you add or modify a configuration property. This ensures all four sources (defaults, file, env, flags) work seamlessly together.