应用服务配置读取、内容处理和使用。
仓库🌟: https://github.com/bobacgo/kit
涉及文件:
├── app
│ ├── conf
│ │ ├── config.go
│ │ └── load.go
App 应用配置都有功能哪些?
1.支持热加载
2.多种文件类型
3.配置值格式校验
4.配置值默认值
5.配置特殊类型解析
6.支持配置值脱敏输出
7.支持多配置文件
优先级: (相同key)
1.主配置文件优先级最高
2.configs 数组索引越小优先级越高
配置文件 config.yaml
# 服务的基本信息
name: examples-service
version: '1.0.0'
env: dev
configs: # 支持多个配置文件
- ./deploy/v1.0.0/db.yaml
- ./deploy/v1.0.0/logger.yaml
- ./deploy/v1.0.0/redis.yaml
# http 服务 和 rpc服务 配置
server:
http:
addr: '0.0.0.0:8080'
timeout: 1s
rpc:
addr: '0.0.0.0:9080'
timeout: 1s
# web 服务需要的安全配置
security:
ciphertext: # 前端密码加密传输
isCiphertext: false
cipherKey: YpC5wIRf4ZuMvd4f
jwt:
secret: YpC5wIRf4ZuMvd4f
issuer: gogo-admin
cacheKeyPrefix: "admin:login_token"
localCache:
maxSize: 500MB
# 数据库配置
db:
default:
dryRun: false # 是否空跑 (用于调试,数据不会写入数据库)
source: admin:root@tcp(127.0.0.1:3306)/mall-ums?charset=utf8mb4&parseTime=True&loc=Local
slowThreshold: 1
maxLifeTime: 1
maxOpenConn: 100
maxIdleConn: 30
# 业务相关
service:
errAttemptLimit: 5
kafka:
addr: '127.0.0.1:9092'
timeout: 1s
# ====================================
# registry
registry:
addr: '127.0.0.1:2379'
对应的结构体设计
配置文件分成两部分
-
基础部分(框架基本组件需要的配置对象相对固定)
-
可变部分(业务配置,对不同服务个性化配置支持)
type App[T any] struct {
Basic `mapstructure:",squash"`
Service T `mapstructure:"service"` // 应用自己的其他配置
}
// Basic 服务必要的配置文件
type Basic struct {
Name string `mapstructure:"name" validate:"required"` // 服务名称
Version string `mapstructure:"version" validate:"semver"` // 服务版本
Env enum.EnvType `mapstructure:"env" validate:"oneof=dev test prod"`
// 和主配置文件的在同一个目录可以只写文件名加后缀
Configs []string `mapstructure:"configs"` // 其他配置文件的路径
// 注册中心的地址
Registry Transport `mapstructure:"registry"`
Server struct {
Http Transport `mapstructure:"http"`
Rpc Transport `mapstructure:"rpc"` // rpc 端口号没有指定,就是http端口号+1000
} `mapstructure:"server"`
Security security.Config `mapstructure:"security"`
Logger logger.Config `mapstructure:"logger"`
DB map[string]db.Config `mapstructure:"db"` // 支持多数据源 default key 必须存在
LocalCache cache.LocalCacheConf `mapstructure:"localCache" yaml:"localCache"`
Redis cache.RedisConf `mapstructure:"redis"`
}
type Transport struct {
Addr string `mapstructure:"addr"` // 监听地址 0.0.0.0:80
Timeout types.Duration `mapstructure:"timeout" validate:"duration" default:"5s"` // 超时时间 1s
}
BasicConf 由各个组件提供配置的结合:
-
Registry 注册中心连接配置
-
Server http、rpc 启动参数配置
-
Security 安全相关配置
-
Logger 日志配置
-
DB 数据库配置,map 支持多个实例配置
-
LocalCache 配置
-
Redis 参数配置
ServiceConf 服务自己的配置,框架只要知道 struct 的定义,通过这个定义去解析它。
// 提供了设置和获取的方法
var (
basicCfg atomic.Value
serviceCfg atomic.Value
)
func GetBasicConf() Basic {
cfg, _ := basicCfg.Load().(Basic)
return cfg
}
func GetServiceConf[T any]() T {
v, _ := serviceCfg.Load().(T)
return v
}
func SetApp[T any](appCfg *App[T]) {
if appCfg == nil {
return
}
basicCfg.Store(appCfg.Basic)
serviceCfg.Store(appCfg.Service)
}
服务在运行时,会读取配置文件的值也同时可能去修改配置,我们这样用 atomic.Value 来保证并发读写取安全。
配置中我们自定义了两个类型
-
types.ByteSize (512m、512MB)
-
types.Duration("300ms", "-1.5h" or "2h54m)
配置文件提供对人类友好的可读性的方式,将其解析成程序需要的值。
服务对象(app.New)创建时首先要先去加载配置文件
-
泛型 T 是自定义配置的 struct。
-
提供一个回调函数,文件配置有更改就会触发。
-
这里还看到如果解析配置文件出错就会 panic,配置文件出错后面的组件依赖不好进行。
我们来看 LoadApp
// LoadApp 加载配置文件
// 配置文件有变化时,会自动全部重新加载配置文件
// 优先级: (相同key)
//
// 1.主配置文件优先级最高
// 2.configs 数组索引越小优先级越高
func LoadApp[T any](filepath string, onChange func(e fsnotify.Event)) (*App[T], error) {
cfg := new(App[T])
if onChange != nil {
onChange = reload[T](filepath, onChange)
}
// 加载主配置文件
if err := Load(filepath, cfg, onChange); err != nil {
return nil, err
}
// 加载其他配置文件
// configs 数组索引越小优先级越高
for i := len(cfg.Configs) - 1; i >= 0; i-- {
configPath := cfg.Configs[i] // 捕获循环变量
if err := Load(configPath, cfg, onChange); err != nil {
return nil, err
}
}
// 主配置文件优先级最高,最后加载以覆盖其他配置
if len(cfg.Configs) > 0 {
if err := Load(filepath, cfg, nil); err != nil {
return nil, err
}
}
if err := validator.Struct(cfg); err != nil {
return nil, err
}
cfg = tag.Default(cfg) // 带有默认值 tag 标签赋值
SetApp(cfg)
return cfg, nil
}
reload[T](filepath, onChange) 如果任何配置文件有变更就会重新调用 LoadApp方法再次加载一遍配置。
数据解析后的结果是放 map 里,所以在不同的文件里有冲突的key,就会按照先后顺序覆盖前一个key
-
第一次Load关键的是读取出 configs 其他配置文件的位置。
-
如果configs有多个文件那就倒序把它读取出来。
-
如果configs多个文件已经加载完,再重新加载主配置。
-
使用 validator.Struct 对 config struct 带有 "validate" 标签进行校验,保证配置值按照要求配置。
读取之后的配置值通过 SetApp 存放在,basicCfg 和 serviceCfg。
我们来看基于 viper 实现的 Load 方法
func Load[T any](filepath string, cfg *T, onChange func(e fsnotify.Event)) error {
vpr := viper.New()
vpr.SetConfigFile(filepath)
vpr.ReadInConfig()
if err := vpr.ReadInConfig(); err != nil {
return err
}
if err := vpr.Unmarshal(cfg); err != nil {
return err
}
if onChange != nil {
vpr.WatchConfig()
vpr.OnConfigChange(func(e fsnotify.Event) {
onChange(e)
})
}
return nil
}
使用到它主要的两个特性:
-
支持多种文件类型(JSON、TOML、YAML、HCL、envfile )。
-
监听文件修改。
配置都解析和加载完成后
// 提供一个脱敏标签(mask)的配置文件
// 扫描标签 并用 ** 替换
maskConf := tag.Desensitize(cfg)
cfgData, _ := yaml.Marshal(maskConf)
slog.Info("local config info\n" + string(cfgData))
把它输出出来,用于方便调试。因为这里会涉及密码或隐私数据泄露问题,所以我们提供了一个 tag "mask"。如果这个字段需要打码,支持正则替换,用*号,如果没有给正则值,就会替换中间的3/1。
配置文件涉及的两个自定义 tag "default","mask", 代码位置如下:
├── pkg
│ ├── tag # pkg/tag 这个包,解析自定义struct tag 相关的
│ │ ├── default.go
│ │ ├── mask.go
│ │ └── tag.go
好了,配置模块的功能介绍到这里就结束了。下次见!
欢迎留言讨论,点赞👍分享🌹