配置管理
mcfg 是 Maltose 框架提供的配置管理组件,提供了简洁的、面向实例的接口和可扩展的适配器模式。
特性
- 多格式支持: 默认支持
yaml、json、toml常见配置文件格式。 - 适配器模式: 通过
Adapter接口,可以轻松扩展支持其他配置源,如环境变量、远程配置中心(Nacos, Apollo)等。 - 实例管理: 支持通过不同的名称获取不同的配置实例,方便管理多个配置文件。
- 自动加载: 默认会自动在
config目录下查找并加载名为config.yaml(或实例名对应的文件)的配置文件。 - 类型安全: 获取的配置项会被包装为
*mvar.Var类型,可以方便地转换为各种 Go 内置类型。
快速上手
mcfg 的使用非常直观。假设您在项目根目录下的 config/config.yaml 文件中定义了如下配置:
# file: config/config.yaml
server:
name: "my-app"
port: 9000
database:
mysql:
host: "127.0.0.1"
port: "3306"获取配置
您可以通过 m.Config() 获取默认的配置实例,并使用 Get 方法来读取配置项。
package main
import (
"context"
"fmt"
"github.com/graingo/maltose/frame/m"
)
func main() {
ctx := context.Background()
// 获取默认配置实例
cfg := m.Config()
// 获取服务端口,并转换为 int 类型
// Get 方法的第二个参数是默认值,如果配置项不存在则返回默认值
port, err := cfg.Get(ctx, "server.port", 8080)
if err != nil {
panic(err)
}
fmt.Println("Server Port:", port.Int()) // 输出: 9000
// 获取数据库主机地址
dbHost, err := cfg.Get(ctx, "database.mysql.host")
if err != nil {
panic(err)
}
fmt.Println("Database Host:", dbHost.String()) // 输出: 127.0.0.1
// 获取一个不存在的配置,将会使用提供的默认值
logLevel, err := cfg.Get(ctx, "log.level", "info")
if err != nil {
panic(err)
}
fmt.Println("Log Level:", logLevel.String()) // 输出: info
}类型安全的便捷方法
除了通用的 Get 方法,mcfg 还提供了一系列类型安全的便捷方法,让您可以直接获取特定类型的配置值:
// 获取字符串
appName := cfg.GetString(ctx, "server.name") // "my-app"
// 获取整数
port := cfg.GetInt(ctx, "server.port") // 9000
// 获取布尔值
debug := cfg.GetBool(ctx, "server.debug") // true/false
// 获取浮点数
ratio := cfg.GetFloat64(ctx, "server.ratio") // 0.5
// 获取时间长度
timeout := cfg.GetDuration(ctx, "server.timeout") // 30s
// 获取字符串数组
hosts := cfg.GetStringSlice(ctx, "server.allowed_hosts") // []string{"localhost", "127.0.0.1"}优点:
- 类型安全,直接返回 Go 原生类型
- 代码更简洁,无需手动类型转换
- 如果配置项不存在或类型不匹配,返回该类型的零值
配置结构体映射
对于复杂的配置,mcfg.Struct() 方法可以将配置自动映射到结构体,提供更好的类型安全性和代码可读性:
// 定义配置结构体
type ServerConfig struct {
Name string `yaml:"name"`
Port int `yaml:"port"`
Debug bool `yaml:"debug"`
Timeout int `yaml:"timeout"`
}
type AppConfig struct {
Server ServerConfig `yaml:"server"`
Database map[string]any `yaml:"database"`
}
// 将配置映射到结构体
var appCfg AppConfig
err := cfg.Struct(ctx, &appCfg, "")
if err != nil {
panic(err)
}
// 访问配置
fmt.Println("Server Name:", appCfg.Server.Name)
fmt.Println("Server Port:", appCfg.Server.Port)
// 也可以只映射部分配置
var serverCfg ServerConfig
err = cfg.Struct(ctx, &serverCfg, "server")
if err != nil {
panic(err)
}使用场景:
- 配置项较多且结构清晰
- 需要类型安全的配置访问
- 希望利用结构体标签进行验证
- 需要在初始化时一次性加载所有配置
多实例管理
如果您的项目需要加载多个配置文件,可以使用 m.Config(name) 来获取具名的配置实例。
例如,有一个 config/redis.yaml 配置文件:
# file: config/redis.yaml
host: "127.0.0.1"
port: 6379您可以这样获取它的配置:
// 获取名为 "redis" 的配置实例,
// 它会自动加载 config/redis.yaml 文件
redisCfg := m.Config("redis")
redisHost, _ := redisCfg.Get(ctx, "host")
fmt.Println("Redis Host:", redisHost.String()) // 输出: 127.0.0.1适配器 (Adapter)
mcfg 的核心是 Adapter 接口,它定义了配置源的行为。默认使用的是文件适配器 AdapterFile。
// Adapter 接口定义
type Adapter interface {
Get(ctx context.Context, pattern string) (any, error)
Data(ctx context.Context) (map[string]any, error)
Available(ctx context.Context, resource ...string) bool
}您可以实现自己的 Adapter 来对接不同的配置中心,例如 Nacos。
// 这是一个简化的 Nacos 适配器示例
type NacosAdapter struct {
// ... Nacos client and config ...
}
func (a *NacosAdapter) Get(ctx context.Context, pattern string) (any, error) {
// 从 Nacos 获取配置
}
func (a *NacosAdapter) Data(ctx context.Context) (map[string]any, error) {
// 从 Nacos 获取所有配置
}
// ...
// 使用自定义适配器
nacosAdapter := &NacosAdapter{ ... }
customCfg := mcfg.NewWithAdapter(nacosAdapter)
// 后续使用与默认配置完全一致
value, _ := customCfg.Get(ctx, "some.key.from.nacos")Maltose 在 contrib/config 中已经提供了一些常用的配置中心适配器,您可以直接使用。
清除缓存
如果您使用了支持缓存的自定义适配器,可以手动清除缓存:
// 清除配置缓存
// 注意:这会触发适配器重新加载配置
cfg.ClearCache()使用场景:
- 开发环境下实现配置热重载
- 配置中心推送了配置更新通知
- 需要强制刷新配置
注意事项:
- 默认的文件适配器不使用缓存,调用此方法无效果
- 生产环境不建议频繁调用,可能影响性能
- 如果使用远程配置中心,清除缓存会触发网络请求
配置加载钩子 (Hooks)
配置钩子(Hook)提供了一种强大的机制,允许您在配置数据从适配器(如文件、Nacos)加载后,但在最终返回给应用程序之前,对其进行拦截和处理。这对于实现配置的动态修改、数据校验、解密或从多个来源合并配置等高级场景非常有用。
mcfg 支持两种类型的钩子:
- 无状态钩子 (
ConfigHookFunc): 一个简单的函数,适用于不需要跨次调用保持状态的简单数据转换。 - 有状态钩子 (
StatefulHook): 一个接口,其实现可以拥有自己的内部状态(如缓存、sync.Once)。这是处理需要缓存结果的昂贵操作(如网络请求)的推荐方式,可以避免使用全局变量,使代码更整洁、更易于测试。
最佳实践:使用 StatefulHook 实现远程配置引导
一个非常常见的场景是:使用本地配置文件来存储远程配置中心(如 Nacos)的连接信息,然后在应用启动时,连接到配置中心,拉取业务配置,并将其与本地配置合并。
StatefulHook 是实现此需求的完美方案。
1. 定义 StatefulHook 实现
我们创建一个 NacosHook 结构体,它实现了 mcfg.StatefulHook 接口,并将所有与 Nacos 相关的状态(sync.Once, 缓存的数据和错误)都封装在内部。
// file: internal/logic/hook/nacos.go
package hook
import (
"context"
"fmt"
"sync"
"github.com/graingo/maltose/contrib/config/nacos"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
"github.com/spf13/viper"
)
// NacosHook 实现了 mcfg.StatefulHook 接口.
// 它封装了从 Nacos 获取配置的逻辑,并使用内部状态来缓存结果,确保昂贵的网络操作只执行一次。
type NacosHook struct {
once sync.Once
data map[string]any
err error
}
// Hook 是接口的实现. 它在首次调用时从 Nacos 获取配置,
// 并在后续调用中返回缓存的数据,然后与传入的本地配置进行合并。
func (nh *NacosHook) Hook(ctx context.Context, data map[string]any) (map[string]any, error) {
nh.once.Do(func() {
nacosConfigMap, ok := data["nacos"]
if !ok {
// 如果本地配置中没有 nacos 节点, 初始化为空 map 并直接返回
nh.data = make(map[string]any)
return
}
cfg, ok := nacosConfigMap.(map[string]any)
if !ok {
nh.err = fmt.Errorf("'nacos' config is not a valid map")
return
}
// 此处可以添加更健壮的类型断言和错误处理
// serverConfigs := []constant.ServerConfig{ ... }
// clientConfig := constant.ClientConfig{ ... }
// configParam := vo.ConfigParam{ ... }
// 创建 Nacos 适配器
// nacosAdapter, err := nacos.New(ctx, nacos.Config{ ... })
// if err != nil {
// nh.err = fmt.Errorf("failed to create nacos adapter: %w", err)
// return
// }
// 从 Nacos 获取远程配置并缓存在 NacosHook 实例的字段中
// remoteData, err := nacosAdapter.Data(ctx)
// if err != nil {
// nh.err = fmt.Errorf("failed to fetch data from nacos: %w", err)
// return
// }
// nh.data = remoteData
})
if nh.err != nil {
return nil, nh.err
}
// 每次调用都执行合并逻辑
v := viper.New()
_ = v.MergeConfigMap(data) // 1. 合并本地配置
_ = v.MergeConfigMap(nh.data) // 2. 将缓存的 Nacos 配置覆盖上去
return v.AllSettings(), nil
}2. 注册钩子
在您的应用初始化逻辑中(例如 main.go 或 cmd/root.go 的 init 函数),将 NacosHook 的实例注册到 mcfg。
import (
"my-app/internal/logic/hook"
"github.com/graingo/maltose/os/mcfg"
)
func init() {
// ... 其他初始化代码 ...
// 注册钩子时,传入 NacosHook 的一个新实例
mcfg.RegisterAfterLoadHook(&hook.NacosHook{})
}通过这种模式,您可以用一种非常优雅和健壮的方式扩展配置加载逻辑,而不会污染全局作用域,也无需修改框架的核心代码。