writing-skills
writing-skills
Use when creating new skills, editing existing skills, or verifying skills work before deployment
go-configurationManages application configuration in Go with environment variables, YAML/JSON parsing, validation, defaults, and hierarchy for multi-environment deployments.
Independent GitHub snapshot · Automated classification · Original documentation governs
name: go-configuration description: Manages application configuration in Go with environment variables, YAML/JSON parsing, validation, defaults, and hierarchy for multi-environment deployments. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: go role: implementation scope: implementation output-format: code content-types:
Senior platform engineer implementing robust configuration management for Go applications. This skill covers environment variables, YAML/JSON parsing, validation, defaults, secret handling, and multi-environment configuration hierarchy.
*Config pointer, not a struct with settersUse this skill when:
Avoid this skill for:
Define the Configuration Struct — Group related settings into logical structs. Checkpoint: Every field has a zero-value that is invalid — forces explicit loading.
Implement Loading Logic — Load from multiple sources in priority order. Checkpoint: Higher-priority sources override lower-priority ones.
Validate Configuration — Check all required fields and valid ranges at load time. Checkpoint: Invalid config causes immediate startup failure with a descriptive error.
Handle Secrets Separately — Load secrets from environment or secrets manager only. Checkpoint: No secrets in config files, logs, or error messages.
Make Configuration Immutable — Return a *Config pointer after loading — no setters.
Checkpoint: Configuration cannot be modified after LoadConfig() returns.
Configuration should load from a well-defined hierarchy with validation.
// ❌ BAD: configuration scattered across the codebase
package main
var (
dbHost = "localhost" // hardcoded default — no validation
dbPort = 5432 // magic number
dbName = "myapp" // hardcoded
apiKey = os.Getenv("API_KEY") // nil if not set — no error
logLevel = os.Getenv("LOG_LEVEL") // "debug" if not set — no validation
maxRetries = 3 // magic number
)
func main() {
// No validation — invalid config causes runtime failures
runServer()
}
func runServer() {
// Business logic depends on global vars — impossible to test independently
connStr := fmt.Sprintf("postgres://%s:%d/%s", dbHost, dbPort, dbName)
// ...
}
What's wrong:
os.Getenv returns empty string if not set — no error, no validationLOG_LEVEL = "blah" is accepted// package: config
// Config holds all application configuration.
// Zero values are invalid — all fields must be explicitly loaded.
type Config struct {
Server ServerConfig
Database DatabaseConfig
Logging LoggingConfig
API APIConfig
}
// ServerConfig holds HTTP server configuration.
type ServerConfig struct {
Addr string `mapstructure:"addr"`
Port int `mapstructure:"port"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
}
// DatabaseConfig holds database connection settings.
type DatabaseConfig struct {
DSN string `mapstructure:"dsn"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
}
// LoggingConfig holds logging configuration.
type LoggingConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"` // "json" or "text"
}
// APIConfig holds external API configuration.
type APIConfig struct {
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"` // secret — only from env
Timeout time.Duration `mapstructure:"timeout"`
MaxRetries int `mapstructure:"max_retries"`
}
// LoadConfig loads configuration from a file and environment variables.
// Priority: file < environment variables.
// Returns an error if required fields are missing or invalid.
func LoadConfig(cfgFile string) (*Config, error) {
v := viper.New()
// Set defaults
v.SetDefault("server.addr", "0.0.0.0")
v.SetDefault("server.port", 8080)
v.SetDefault("server.read_timeout", 15*time.Second)
v.SetDefault("server.write_timeout", 30*time.Second)
v.SetDefault("server.idle_timeout", 120*time.Second)
v.SetDefault("database.max_open_conns", 25)
v.SetDefault("database.max_idle_conns", 10)
v.SetDefault("logging.level", "info")
v.SetDefault("logging.format", "json")
v.SetDefault("api.timeout", 30*time.Second)
v.SetDefault("api.max_retries", 3)
// Configure file loading
if cfgFile != "" {
v.SetConfigFile(cfgFile)
} else {
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath("/etc/myapp")
v.AddConfigPath("$HOME/.myapp")
v.AddConfigPath(".")
}
// Read config file (ignore "not found" — file is optional)
if err := v.ReadInConfig(); err != nil {
if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return nil, fmt.Errorf("read config file: %w", err)
}
}
// Environment variable overrides
v.AutomaticEnv()
v.SetEnvPrefix("APP")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Bind environment variables to config keys
v.BindEnv("server.addr", "SERVER_ADDR")
v.BindEnv("server.port", "SERVER_PORT")
v.BindEnv("database.dsn", "DATABASE_URL")
v.BindEnv("api.api_key", "API_KEY") // secret — only from env
v.BindEnv("logging.level", "LOG_LEVEL")
v.BindEnv("logging.format", "LOG_FORMAT")
// Decode into struct
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
}
// Validate
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("validate config: %w", err)
}
return &cfg, nil
}
// Validate checks that all required configuration is present and valid.
func (c *Config) Validate() error {
var errs []error
// Server validation
if c.Server.Port < 1 || c.Server.Port > 65535 {
errs = append(errs, fmt.Errorf("server.port must be 1-65535, got %d", c.Server.Port))
}
// Database validation
if c.Database.DSN == "" {
errs = append(errs, errors.New("database.dsn is required"))
}
if c.Database.MaxOpenConns < 1 {
errs = append(errs, errors.New("database.max_open_conns must be >= 1"))
}
// Logging validation
switch c.Logging.Level {
case "debug", "info", "warn", "error", "fatal":
default:
errs = append(errs, fmt.Errorf("logging.level must be one of: debug, info, warn, error, fatal; got %q", c.Logging.Level))
}
if c.Logging.Format != "json" && c.Logging.Format != "text" {
errs = append(errs, fmt.Errorf("logging.format must be 'json' or 'text'; got %q", c.Logging.Format))
}
// API validation
if c.API.APIKey == "" {
errs = append(errs, errors.New("api.api_key is required (set via APP_API_KEY env var)"))
}
if len(errs) > 0 {
return multiError(errs)
}
return nil
}
// multiError collects multiple validation errors into one.
type multiError []error
func (e multiError) Error() string {
var msgs []string
for _, err := range e {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
Why this works:
viper handles file loading + env var merging automaticallySetEnvPrefix("APP") means SERVER_PORT becomes APP_SERVER_PORTSetEnvKeyReplacer maps dots to underscores (server.addr → SERVER_ADDR)Validate() checks all fields at load time — startup fails fast*Config is immutable — no public settersSecrets should never come from config files — only from environment or a secrets manager.
# ❌ BAD: secrets in a YAML config file — committed to git by accident
server:
addr: "0.0.0.0"
port: 8080
database:
dsn: "postgres://admin:SuperSecret123!@db.example.com:5432/myapp"
api:
api_key: "sk-proj-abc123def456ghi789"
secret: "whK3mP9xR2vL8nQ1"
What's wrong:
// package: config
// LoadSecrets loads secrets from environment variables.
// Secrets are NEVER loaded from config files.
func LoadSecrets() (*Secrets, error) {
s := &Secrets{}
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
return nil, errors.New("DATABASE_URL is required")
}
s.DSN = dsn
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
return nil, errors.New("API_KEY is required")
}
s.APIKey = apiKey
secret := os.Getenv("JWT_SECRET")
if secret == "" {
return nil, errors.New("JWT_SECRET is required")
}
s.JWTSecret = secret
return s, nil
}
// Secrets holds sensitive configuration values.
// Zero values are invalid — all fields must be loaded.
type Secrets struct {
DSN string
APIKey string
JWTSecret string
}
// DSNWithPassword returns the database DSN with the password masked.
func (s *Secrets) DSNMasked() string {
// Replace password with "***" for logging
masked := s.DSN
if idx := strings.Index(masked, "://"); idx != -1 {
if after := masked[idx+3:]; strings.Contains(after, "@") {
parts := strings.SplitN(after, "@", 2)
masked = masked[:idx+3] + "***@" + parts[1]
}
}
return masked
}
// package: main
func main() {
// Load configuration (non-sensitive settings)
cfg, err := config.LoadConfig("config.yaml")
if err != nil {
log.Fatalf("load config: %v", err)
}
// Load secrets separately (only from environment or secrets manager)
secrets, err := config.LoadSecrets()
if err != nil {
log.Fatalf("load secrets: %v", err)
}
// Log masked config for debugging
log.Printf("starting with config: %s", secrets.DSNMasked())
// Use cfg and secrets together
svc := NewService(cfg, secrets)
svc.Run()
}
Why this works:
DATABASE_URL, API_KEY, JWT_SECRET are standard env var conventionsDSNMasked() safely displays connection strings in logsConfiguration should be self-documenting with clear defaults and validation.
// ❌ BAD: no documentation of what configuration options exist
type Config struct {
A string
B int
C bool
D string
}
func Load() *Config {
return &Config{
A: "x",
B: 42,
C: true,
D: "z",
}
}
What's wrong:
A, B, C, D are meaningless — no documentation// package: config
// Config holds all application settings.
// Each field has an associated environment variable and a documented default.
type Config struct {
// Server settings
Server ServerConfig
// Database settings
Database DatabaseConfig
// Logging settings
Logging LoggingConfig
}
// ServerConfig holds HTTP server settings.
type ServerConfig struct {
// Addr is the network interface to listen on.
// Env: SERVER_ADDR
// Default: "0.0.0.0"
Addr string
// Port is the TCP port to listen on.
// Env: SERVER_PORT
// Default: 8080
// Range: 1-65535
Port int
// ReadTimeout is the maximum duration for reading the entire request,
// including the body.
// Env: SERVER_READ_TIMEOUT
// Default: "15s"
ReadTimeout time.Duration
// WriteTimeout is the maximum duration before timing out writes.
// Env: SERVER_WRITE_TIMEOUT
// Default: "30s"
WriteTimeout time.Duration
// IdleTimeout is the maximum amount of time to wait for the next request.
// Env: SERVER_IDLE_TIMEOUT
// Default: "120s"
IdleTimeout time.Duration
}
// DatabaseConfig holds database connection settings.
type DatabaseConfig struct {
// DSN is the PostgreSQL connection string.
// Env: DATABASE_URL
// Required: true
DSN string
// MaxOpenConns is the maximum number of open connections to the database.
// Env: DB_MAX_OPEN_CONNS
// Default: 25
MaxOpenConns int
// MaxIdleConns is the maximum number of idle connections to the database.
// Env: DB_MAX_IDLE_CONNS
// Default: 10
MaxIdleConns int
}
// LoggingConfig holds logging settings.
type LoggingConfig struct {
// Level is the minimum log level.
// Env: LOG_LEVEL
// Default: "info"
// Values: debug, info, warn, error, fatal
Level string
// Format is the log output format.
// Env: LOG_FORMAT
// Default: "json"
// Values: json, text
Format string
}
// DefaultConfig returns a Config with all defaults set.
// Use this as a starting point before merging with file/env configuration.
func DefaultConfig() *Config {
return &Config{
Server: ServerConfig{
Addr: "0.0.0.0",
Port: 8080,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
},
Database: DatabaseConfig{
MaxOpenConns: 25,
MaxIdleConns: 10,
},
Logging: LoggingConfig{
Level: "info",
Format: "json",
},
}
}
Why this works:
// Env: comment documents the environment variable name// Default: comment documents the default value// Range: and // Values: document valid ranges and accepted valuesDefaultConfig() provides a clear starting point for mergingos.Getenv directly in business logic*Config as a dependencyos.Getenv directly in business logic — always use the Config struct| Skill | Purpose |
|---|---|
best-practices | Go idioms, naming, and code organization |
cloud-development | Cloud-native configuration with Kubernetes ConfigMaps and Secrets |
deployment-patterns | Configuration in containerized environments |
Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.