From eaf99e158273873ec6f1f55f75d07d0fea960a86 Mon Sep 17 00:00:00 2001 From: zenfun Date: Fri, 12 Dec 2025 22:50:54 +0800 Subject: [PATCH] feat(config): migrate configuration management to viper Replace manual environment variable parsing with Viper to support file-based configuration. - Enable loading config from `config.yaml` or `./config/config.yaml` - Allow custom config path via `EZ_CONFIG_FILE` environment variable - Bind existing environment variables to maintain backward compatibility - Update documentation with configuration examples and precedence rules --- README.md | 18 ++++++++ go.mod | 10 ++++- go.sum | 25 +++++++++-- internal/config/config.go | 90 ++++++++++++++++++++++++--------------- 4 files changed, 104 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index d169bb0..45d55e3 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,24 @@ EZ-API 是"大脑"。它管理着事实的来源 (Source of Truth)。 | `EZ_LOG_BATCH_SIZE` | `10` | 单次 DB 事务写入的日志数量。 | | `EZ_LOG_LEVEL` | `info` | 日志级别,支持 `debug`、`info`、`warn`、`error`。 | +配置读取优先级:环境变量 > 配置文件 > 默认值。通过 [Viper](https://github.com/spf13/viper) 支持 `./config.yaml`(或 `./config/config.yaml`),也可用 `EZ_CONFIG_FILE` 指定路径。示例: + +```yaml +server: + port: 8080 +postgres: + dsn: host=localhost user=postgres password=postgres dbname=ezapi port=5432 sslmode=disable +redis: + addr: localhost:6379 + db: 0 +log: + batch_size: 10 + flush_ms: 1000 + queue_capacity: 10000 +auth: + jwt_secret: change_me_in_production +``` + ## 运行方式 ### 本地运行 diff --git a/go.mod b/go.mod index a48b20d..8e9eae0 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gin-gonic/gin v1.11.0 github.com/redis/go-redis/v9 v9.17.2 github.com/rs/zerolog v1.34.0 + github.com/spf13/viper v1.21.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 @@ -21,6 +22,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.22.3 // indirect @@ -36,6 +38,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -46,7 +49,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -55,6 +57,12 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect diff --git a/go.sum b/go.sum index 4eeb6e5..d665fbd 100644 --- a/go.sum +++ b/go.sum @@ -13,12 +13,15 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= @@ -62,6 +65,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -86,8 +91,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -119,6 +124,18 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -129,6 +146,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= diff --git a/internal/config/config.go b/internal/config/config.go index 63942c4..192d08d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,9 +1,12 @@ package config import ( + "fmt" "os" - "strconv" + "strings" "time" + + "github.com/spf13/viper" ) type Config struct { @@ -39,50 +42,67 @@ type LogConfig struct { } func Load() (*Config, error) { - return &Config{ + v := viper.New() + + v.SetDefault("server.port", "8080") + v.SetDefault("postgres.dsn", "host=localhost user=postgres password=postgres dbname=ezapi port=5432 sslmode=disable") + v.SetDefault("redis.addr", "localhost:6379") + v.SetDefault("redis.password", "") + v.SetDefault("redis.db", 0) + v.SetDefault("log.batch_size", 10) + v.SetDefault("log.flush_ms", 1000) + v.SetDefault("log.queue_capacity", 10000) + v.SetDefault("auth.jwt_secret", "change_me_in_production") + + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.AutomaticEnv() + + _ = v.BindEnv("server.port", "EZ_API_PORT") + _ = v.BindEnv("postgres.dsn", "EZ_PG_DSN") + _ = v.BindEnv("redis.addr", "EZ_REDIS_ADDR") + _ = v.BindEnv("redis.password", "EZ_REDIS_PASSWORD") + _ = v.BindEnv("redis.db", "EZ_REDIS_DB") + _ = v.BindEnv("log.batch_size", "EZ_LOG_BATCH_SIZE") + _ = v.BindEnv("log.flush_ms", "EZ_LOG_FLUSH_MS") + _ = v.BindEnv("log.queue_capacity", "EZ_LOG_QUEUE") + _ = v.BindEnv("auth.jwt_secret", "EZ_JWT_SECRET") + + if configFile := os.Getenv("EZ_CONFIG_FILE"); configFile != "" { + v.SetConfigFile(configFile) + } else { + v.SetConfigName("config") + v.SetConfigType("yaml") + v.AddConfigPath(".") + v.AddConfigPath("./config") + } + + if err := v.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return nil, fmt.Errorf("read config: %w", err) + } + } + + cfg := &Config{ Server: ServerConfig{ - Port: getEnv("EZ_API_PORT", "8080"), + Port: v.GetString("server.port"), }, Postgres: PostgresConfig{ - DSN: getEnv("EZ_PG_DSN", "host=localhost user=postgres password=postgres dbname=ezapi port=5432 sslmode=disable"), + DSN: v.GetString("postgres.dsn"), }, Redis: RedisConfig{ - Addr: getEnv("EZ_REDIS_ADDR", "localhost:6379"), - Password: getEnv("EZ_REDIS_PASSWORD", ""), - DB: getEnvInt("EZ_REDIS_DB", 0), + Addr: v.GetString("redis.addr"), + Password: v.GetString("redis.password"), + DB: v.GetInt("redis.db"), }, Log: LogConfig{ - BatchSize: getEnvInt("EZ_LOG_BATCH_SIZE", 10), - FlushInterval: getEnvDuration("EZ_LOG_FLUSH_MS", 1000), - QueueCapacity: getEnvInt("EZ_LOG_QUEUE", 10000), + BatchSize: v.GetInt("log.batch_size"), + FlushInterval: time.Duration(v.GetInt("log.flush_ms")) * time.Millisecond, + QueueCapacity: v.GetInt("log.queue_capacity"), }, Auth: AuthConfig{ - JWTSecret: getEnv("EZ_JWT_SECRET", "change_me_in_production"), + JWTSecret: v.GetString("auth.jwt_secret"), }, - }, nil -} - -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value } - return fallback -} -func getEnvInt(key string, fallback int) int { - if value, ok := os.LookupEnv(key); ok { - if i, err := strconv.Atoi(value); err == nil { - return i - } - } - return fallback -} - -func getEnvDuration(key string, fallbackMs int) time.Duration { - if value, ok := os.LookupEnv(key); ok { - if i, err := strconv.Atoi(value); err == nil { - return time.Duration(i) * time.Millisecond - } - } - return time.Duration(fallbackMs) * time.Millisecond + return cfg, nil }