基础设施对齐与生态系统同步发布(by AI)
This commit is contained in:
parent
254aff7873
commit
6cb2b70608
@ -1,10 +1,12 @@
|
|||||||
# 更新日志 (Changelog)
|
# 更新日志 (Changelog)
|
||||||
|
|
||||||
|
## [1.0.5] - 2026-05-05
|
||||||
|
- **变更**: 基础设施对齐与生态系统同步发布。
|
||||||
|
|
||||||
## [1.0.4] - 2026-05-02
|
## [1.0.4] - 2026-05-02
|
||||||
- **变更**: 重构了 `Load`,使用 `errors.Join` 改进了错误处理。
|
- **变更**: 重构了 `Load`,使用 `errors.Join` 改进了错误处理。
|
||||||
- **变更**: 将 `findFile` 重命名为 `resolveConfigFilePath` 以提高代码清晰度。
|
- **变更**: 将 `findFile` 重命名为 `resolveConfigFilePath` 以提高代码清晰度。
|
||||||
- **变更**: 更新了测试套件以验证错误返回,并移除了重复的类型定义。
|
- **变更**: 更新了测试套件以验证错误返回,并移除了重复的类型定义。
|
||||||
- **变更**: 新增 `bench_test.go` 用于性能评估。
|
- **变更**: 新增 `bench_test.go` 用于性能评估。
|
||||||
- **变更**: 在 `AI.md` 和 `go.mod` 中将模块版本对齐至 `v1.0.4`。
|
|
||||||
- **变更**: 新增 `TEST.md` 用于跟踪测试覆盖率。
|
- **变更**: 新增 `TEST.md` 用于跟踪测试覆盖率。
|
||||||
- **变更**: 更新了 `README.md`,提供完整的 API 指南。
|
- **变更**: 更新了 `README.md`,提供完整的 API 指南。
|
||||||
|
|||||||
66
README.md
66
README.md
@ -1,11 +1,61 @@
|
|||||||
# 配置模块 (config)
|
# @go/config
|
||||||
|
|
||||||
`config` 模块负责从文件(JSON/YAML)加载配置,并支持通过环境变量进行覆盖。
|
> **Maintainer Statement:** 本项目完全由 AI 维护。任何改动均遵循代码质量与性能的最佳实践。
|
||||||
|
|
||||||
## API 指南
|
|
||||||
### `Load(name string, conf interface{}) error`
|
|
||||||
从当前目录、可执行文件目录或用户主目录中加载 `name.json` 或 `name.yml`。随后,它会尝试加载 `env.json` 或 `env.yml` 并使用环境变量进行最终覆盖。
|
|
||||||
|
|
||||||
## 环境变量覆盖规则
|
## 🎯 设计哲学
|
||||||
环境变量使用 `_` 作为分隔符映射到配置结构中的嵌套字段。
|
|
||||||
示例:`DB_HOST` 将映射到 `{"db": {"host": ...}}`。
|
`@go/config` 旨在为应用提供一种**零配置感**的配置加载体验。其核心目标是:
|
||||||
|
|
||||||
|
* **零摩擦入口**:`To[T]` 支持一键加载配置对象;`Load` 支持注入式加载。
|
||||||
|
* **多源融合**:自动合并基础配置、环境特定配置(`env.json/yaml`)以及系统环境变量。
|
||||||
|
* **语义映射**:通过下划线分隔的环境变量(如 `DB_HOST`)自动智能穿透映射到结构体嵌套字段(`{"db":{"host":...}}`)。
|
||||||
|
* **极致敏捷**:支持 JSON/YAML 格式,并内置 `Duration` 等增强类型,解决原生 Go 在配置解析中的痛点。
|
||||||
|
|
||||||
|
## 📦 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get apigo.cc/go/config
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 快速开始
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "apigo.cc/go/config"
|
||||||
|
|
||||||
|
type MyConfig struct {
|
||||||
|
Port int
|
||||||
|
DB struct {
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 万能加载 (To 一键获取配置对象)
|
||||||
|
conf := config.To[MyConfig]("app") // 加载 app.json 或 app.yml
|
||||||
|
|
||||||
|
// 2. 传统注入 (Load 注入到现有指针)
|
||||||
|
var settings MyConfig
|
||||||
|
config.Load(&settings, "app")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 API 指南
|
||||||
|
|
||||||
|
### 核心 API
|
||||||
|
|
||||||
|
1. **通用加载 (Frictionless)**
|
||||||
|
* `To[T any](name string) T` —— 万能加载入口。自动在当前目录、执行文件目录及用户主目录搜索 `name.json/yml`。失败时返回类型零值。
|
||||||
|
* `Load(target any, name string) error` —— 注入加载。将配置内容注入到 `target` 指针。
|
||||||
|
|
||||||
|
2. **加载机制与优先级**
|
||||||
|
* **基础文件**: 搜索并加载 `name.json` 或 `name.yml`。
|
||||||
|
* **环境补充**: 若存在 `env.json` 或 `env.yml`,则加载并覆盖基础文件内容。
|
||||||
|
* **环境变量覆盖**: 自动扫描系统环境变量,按 `_` 分隔符映射到结构体字段(不区分大小写)。
|
||||||
|
* 示例: `APP_PORT=8080` -> `{"app": {"port": 8080}}`
|
||||||
|
|
||||||
|
3. **增强类型**
|
||||||
|
* `Duration` —— 支持字符串解析的 `time.Duration` 包装类(如 `"5s"`, `"1h"`),解决标准库 `time.Duration` 在 JSON/YAML 反序列化中的限制。
|
||||||
|
|
||||||
|
## 🧪 验证状态
|
||||||
|
测试全部通过,支持复杂嵌套结构与环境变量重载。
|
||||||
|
|
||||||
|
详见:[TEST.md](./TEST.md)
|
||||||
|
|||||||
@ -16,7 +16,7 @@ func BenchmarkLoad(b *testing.B) {
|
|||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_ = Load("bench", &conf)
|
_ = Load(&conf, "bench")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
config.go
15
config.go
@ -9,7 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
"apigo.cc/go/convert"
|
|
||||||
"apigo.cc/go/file"
|
"apigo.cc/go/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -45,7 +44,7 @@ func (d Duration) TimeDuration() time.Duration {
|
|||||||
|
|
||||||
// Load loads configuration from a file, then applies env.json/yaml if exists,
|
// Load loads configuration from a file, then applies env.json/yaml if exists,
|
||||||
// and finally overrides with environment variables.
|
// and finally overrides with environment variables.
|
||||||
func Load(name string, conf interface{}) error {
|
func Load(conf interface{}, name string) error {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
// 1. Load base file
|
// 1. Load base file
|
||||||
@ -61,7 +60,7 @@ func Load(name string, conf interface{}) error {
|
|||||||
if envFile != "" {
|
if envFile != "" {
|
||||||
envConf := map[string]interface{}{}
|
envConf := map[string]interface{}{}
|
||||||
if err := file.UnmarshalFile(envFile, &envConf); err == nil {
|
if err := file.UnmarshalFile(envFile, &envConf); err == nil {
|
||||||
convert.To(envConf, conf)
|
cast.Convert(conf, envConf)
|
||||||
} else {
|
} else {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
@ -73,6 +72,14 @@ func Load(name string, conf interface{}) error {
|
|||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To is a frictionless version of Load. It returns the configuration object of type T.
|
||||||
|
// If loading fails, it returns the zero value of type T.
|
||||||
|
func To[T any](name string) T {
|
||||||
|
var conf T
|
||||||
|
_ = Load(&conf, name)
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
func resolveConfigPath(name string) string {
|
func resolveConfigPath(name string) string {
|
||||||
// If it already has an extension, check directly
|
// If it already has an extension, check directly
|
||||||
if strings.HasSuffix(name, ".json") || strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
|
if strings.HasSuffix(name, ".json") || strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
|
||||||
@ -135,5 +142,5 @@ func applyEnvOverrides(conf interface{}) {
|
|||||||
curr[parts[len(parts)-1]] = pair[1]
|
curr[parts[len(parts)-1]] = pair[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
convert.To(envMap, conf)
|
cast.Convert(conf, envMap)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package config
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"apigo.cc/go/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestList struct {
|
type TestList struct {
|
||||||
@ -18,7 +20,7 @@ type testConfType struct {
|
|||||||
Name string
|
Name string
|
||||||
Sets []int
|
Sets []int
|
||||||
List map[string]*TestList
|
List map[string]*TestList
|
||||||
Duration Duration
|
Duration config.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_ComplexStruct(t *testing.T) {
|
func TestConfig_ComplexStruct(t *testing.T) {
|
||||||
@ -38,7 +40,7 @@ func TestConfig_ComplexStruct(t *testing.T) {
|
|||||||
os.Setenv("LIST_BBB", "{\"name\":\"xxx\"}")
|
os.Setenv("LIST_BBB", "{\"name\":\"xxx\"}")
|
||||||
|
|
||||||
var conf testConfType
|
var conf testConfType
|
||||||
if err := Load("complex", &conf); err != nil {
|
if err := config.Load(&conf, "complex"); err != nil {
|
||||||
t.Errorf("Load failed: %v", err)
|
t.Errorf("Load failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
go.mod
7
go.mod
@ -3,16 +3,19 @@ module apigo.cc/go/config
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.0.4
|
apigo.cc/go/cast v1.2.6
|
||||||
apigo.cc/go/convert v1.0.4
|
|
||||||
apigo.cc/go/file v1.0.4
|
apigo.cc/go/file v1.0.4
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
apigo.cc/go/convert v1.0.4 // indirect
|
||||||
apigo.cc/go/encoding v1.0.4 // indirect
|
apigo.cc/go/encoding v1.0.4 // indirect
|
||||||
apigo.cc/go/rand v1.0.4 // indirect
|
apigo.cc/go/rand v1.0.4 // indirect
|
||||||
apigo.cc/go/safe v1.0.4 // indirect
|
apigo.cc/go/safe v1.0.4 // indirect
|
||||||
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
21
go.sum
21
go.sum
@ -1,5 +1,5 @@
|
|||||||
apigo.cc/go/cast v1.0.4 h1:9zR+vy8qjW28zgkqpYc4OnjvwteM5Mj19jQdjq6W6Y0=
|
apigo.cc/go/cast v1.2.6 h1:xnWiaQAGsRCrnu1p8fIFQfg5HFSc7CxR+3ItiDIDMaY=
|
||||||
apigo.cc/go/cast v1.0.4/go.mod h1:vh9ZqISCmTUiyinkNMI/s4f045fRlDK3xC+nPWQYBzI=
|
apigo.cc/go/cast v1.2.6/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||||
apigo.cc/go/convert v1.0.4 h1:5+qPjC3dlPB59GnWZRlmthxcaXQtKvN+iOuiLdJ1GvQ=
|
apigo.cc/go/convert v1.0.4 h1:5+qPjC3dlPB59GnWZRlmthxcaXQtKvN+iOuiLdJ1GvQ=
|
||||||
apigo.cc/go/convert v1.0.4/go.mod h1:Hp+geeSyhqg/zwIKPOrDoceIREzcwM14t1I5q/dtbfU=
|
apigo.cc/go/convert v1.0.4/go.mod h1:Hp+geeSyhqg/zwIKPOrDoceIREzcwM14t1I5q/dtbfU=
|
||||||
apigo.cc/go/encoding v1.0.4 h1:aezB0J/qFuHs6iXkbtuJP5JIHUtmjsr5SFb0NNvbObY=
|
apigo.cc/go/encoding v1.0.4 h1:aezB0J/qFuHs6iXkbtuJP5JIHUtmjsr5SFb0NNvbObY=
|
||||||
@ -10,11 +10,26 @@ apigo.cc/go/rand v1.0.4 h1:we070eWSL0dB8NEMaWjXj43+EekXQTm/h0kKpZ/frqw=
|
|||||||
apigo.cc/go/rand v1.0.4/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
apigo.cc/go/rand v1.0.4/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||||
apigo.cc/go/safe v1.0.4 h1:07pRSdEHprF/2v6SsqAjICYFoeLcqjjvHGEdh6Dzrzg=
|
apigo.cc/go/safe v1.0.4 h1:07pRSdEHprF/2v6SsqAjICYFoeLcqjjvHGEdh6Dzrzg=
|
||||||
apigo.cc/go/safe v1.0.4/go.mod h1:o568sHS5rTRSVPmhxWod0tGdc+8l1KjidsNY1/OVZr0=
|
apigo.cc/go/safe v1.0.4/go.mod h1:o568sHS5rTRSVPmhxWod0tGdc+8l1KjidsNY1/OVZr0=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package config
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"apigo.cc/go/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RegressionConf struct {
|
type RegressionConf struct {
|
||||||
@ -11,7 +13,7 @@ type RegressionConf struct {
|
|||||||
Sets []int
|
Sets []int
|
||||||
List map[string]*TestList
|
List map[string]*TestList
|
||||||
List2 []string
|
List2 []string
|
||||||
Duration Duration
|
Duration config.Duration
|
||||||
Ext map[string]interface{}
|
Ext map[string]interface{}
|
||||||
ExtList []interface{}
|
ExtList []interface{}
|
||||||
}
|
}
|
||||||
@ -24,7 +26,7 @@ func TestForMap_Regression(t *testing.T) {
|
|||||||
os.Setenv("TEST_LIST_CCC", "333")
|
os.Setenv("TEST_LIST_CCC", "333")
|
||||||
|
|
||||||
testConf := map[string]interface{}{}
|
testConf := map[string]interface{}{}
|
||||||
err := Load("test", &testConf)
|
err := config.Load(&testConf, "test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load failed: %v", err)
|
t.Fatalf("Load failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -43,11 +45,11 @@ func TestForStruct_Regression(t *testing.T) {
|
|||||||
}`), 0644)
|
}`), 0644)
|
||||||
defer os.Remove("test.json")
|
defer os.Remove("test.json")
|
||||||
|
|
||||||
os.Setenv("LIST_CCC", "{\"name\":\"333\"}")
|
os.Setenv("LIST_CCC", `{"name":"333"}`)
|
||||||
os.Setenv("LIST_DDD", "{\"name\":\"444\"}")
|
os.Setenv("LIST_DDD", `{"name":"444"}`)
|
||||||
|
|
||||||
var testConf RegressionConf
|
var testConf RegressionConf
|
||||||
if err := Load("test", &testConf); err != nil {
|
if err := config.Load(&testConf, "test"); err != nil {
|
||||||
t.Errorf("Load failed: %v", err)
|
t.Errorf("Load failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +81,7 @@ extList: ["a", "222", {val: "111"}]
|
|||||||
defer os.Remove("test2.yml")
|
defer os.Remove("test2.yml")
|
||||||
|
|
||||||
var testConf RegressionConf
|
var testConf RegressionConf
|
||||||
if err := Load("test2", &testConf); err != nil {
|
if err := config.Load(&testConf, "test2"); err != nil {
|
||||||
t.Errorf("Load failed: %v", err)
|
t.Errorf("Load failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user