Viper Environment Configuration Layering

Layer application config with Viper across files, environment variables with key replacer, and overrides bound to flags.

GoConfig Management

Code

snippet
package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/spf13/viper"
)

type ServerConfig struct {
	Host         string
	Port         int
	Timeout      time.Duration
	DatabaseURL  string
	ReplicaHosts []string
}

// precedence: flags > env > config file > defaults
func loadConfig(path string) (*ServerConfig, error) {
	v := viper.New()

	v.SetConfigName("config")
	v.SetConfigType("yaml")
	v.SetConfigFile(path)

	v.SetEnvPrefix("SRV")
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.AutomaticEnv()

	v.SetDefault("host", "0.0.0.0")
	v.SetDefault("port", 8080)
	v.SetDefault("timeout", "5s")
	v.SetDefault("replicaHosts", []string{})

	if err := v.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
			return nil, fmt.Errorf("read config: %w", err)
		}
		// 配置文件缺失不算错:全部回退到环境变量与默认值
	}

	if err := v.Validate(); err != nil {
		return nil, fmt.Errorf("validate: %w", err)
	}

	cfg := &ServerConfig{
		Host:        v.GetString("host"),
		Port:        v.GetInt("port"),
		DatabaseURL: v.GetString("database.url"),
	}
	if raw := v.GetString("timeout"); raw != "" {
		d, err := time.ParseDuration(raw)
		if err != nil {
			return nil, fmt.Errorf("invalid timeout %q: %w", raw, err)
		}
		cfg.Timeout = d
	}
	if replicas := v.GetStringSlice("replicaHosts"); len(replicas) > 0 {
		cfg.ReplicaHosts = replicas
	}

	if cfg.Port < 1 || cfg.Port > 65535 {
		return nil, fmt.Errorf("port %d out of range", cfg.Port)
	}
	return cfg, nil
}

func main() {
	cfg, err := loadConfig("configs/config.yaml")
	fmt.Println(cfg, err)
}