从零构建Go微服务框架: 2.服务配置模块设计

应用服务配置读取、内容处理和使用。

仓库🌟: 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'

对应的结构体设计

配置文件分成两部分

  1. 基础部分(框架基本组件需要的配置对象相对固定)

  2. 可变部分(业务配置,对不同服务个性化配置支持)


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
}

使用到它主要的两个特性:

  1. 支持多种文件类型(JSON、TOML、YAML、HCL、envfile )。

  2. 监听文件修改。

配置都解析和加载完成后

// 提供一个脱敏标签(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

好了,配置模块的功能介绍到这里就结束了。下次见!

欢迎留言讨论,点赞👍分享🌹

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值