reflectConf 是一个基于 Go 语言中 reflect 技术的通用配置解析工具。
reflectConf 可以按照在 struct tag 中配置的规则,将从配管中心、abtest 中心等多处获取的 k-v 格式的配置取出并按 struct tag 中的规则进行解析,然后将解析值写入实例化的 struct 中。
其中解析规则默认支持常用 16 种基础类型以及由它们组成的 slice 和 map[any]struct{},也支持自定义。
使用 reflectconf 需要进行两个操作:
- 为需要进行配置解析的字段配置
struct tag - 调用
reflectconf.FillConfByParamsMap函数
假设有如下 struct ,其中的每项值是又 配管中心、abtest中心和默认值共同决定的,优先级为 abtest > 配管 > 默认值。
// struct
type testCase struct {
TString string
TSlice []float32
TMap1 map[uint32]struct{}
TMap2 map[int64]string
TBool bool
TTimeStamp int64
}
// 配置为:
// 默认值
c1 := map[string]string{
"tString": "abc",
"tSlice": "1.2|3.4|5.6|7.8",
"tMap2": "1:a|2:b|3:c",
"tTimeStamp": "-12h",
}
// 配管
c2 := map[string]string{
"tString": "xyz",
"tMap1": "7|8|9",
"tMap2": "1:a|2:b|3:c",
"tTimeStamp": "-12h",
}
// abtest
c3 := map[string]string{
"tBool": "t",
"tTimeStamp": "-20h",
}此时需要修改 struct ,将配置解析所需的三个参数写到 struct tag
type testCase struct {
TString string `transformer:"direct" conf:"tString" default:"abc" `
TSlice []float32 `transformer:"diSlice" conf:"tSlice" default:"1.2|3.4|5.6|7.8"`
TMap1 map[uint32]struct{} `transformer:"diMapStruct" conf:"tMap1" `
TMap2 map[int64]string `transformer:"confMapIntString" conf:"tMap2" default:"1:a|2:b|3:c" `
TBool bool `transformer:"direct" conf:"tBool" `
TTimeStamp int64 `transformer:"timeDuration" conf:"tTimeStamp" default:"-12h" `
}其中包含了三个 struct tag 配置参数:transformer(解析器名称)、conf(配置名)、default(默认配置值)。
transformer:direct、diSlice、diMapStruct分别表示翻译为 常用 16 种基础类型以及由它们组成的slice和map[any]struct{};confMapIntString、timeDuration为内置的自定义解析器;conf:配置名,也即其在k-v配置字典中的keydefault: 默认值,没有时可以省略
修改 struct 后,只需要调用 FillConfByParamsMap 函数,即可完成配置填充
x := &testCase{}
err := FillConfByParamsMap(x, true, c3, c2, c1)业务中经常会有配置优先级的需求,比如 灰度配置优先级高于正式配置, abtest 配置优先级高于正式配置等。
在 FillConfByParamsMap 除默认配置优先级最低外,其他配置的优先级取决于配置传入的顺序,越靠前、优先级越高。
其函数前名为 **func** FillConfByParamsMap(obj interface{}, useDefault bool, **confMaps ...map[string]string**) error {} ,其中第三个参数支持传入多个配置 map,越靠前、优先级越高。
通用反射转换器
通用反射转换器目前有 3 种,可以将一组类似的类型自动进行类型转换,通用反射转换器 名称及其支持解析的类型如下:
- direct (16种):
string,bool,int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,float32,float64,complex64,complex128 - diSlice(15种):
[]string,[]int,[]int8,[]int16,[]int32,[]int64,[]uint,[]uint8,[]uint16,[]uint32,[]uint64,[]float32,[]float64,[]complex64,[]complex128 - diMapStruct(15种):
map[string]struct{},map[int]struct{},map[int8]struct{},map[int16]struct{},map[int32]struct{},map[int64]struct{},map[uint]struct{},map[uint8]struct{},map[uint16]struct{},map[uint32]struct{},map[uint64]struct{},map[float32]struct{},map[float64]struct{},map[complex64]struct{},map[complex128]struct{}
自定义转换器
自定义转换器 目前有 7 种,主要是解决通用反射转换器无法处理的解析的小众或特殊需求,具体转换器定义见代码 。以 confMapIntInt 转换器为例,演示 自定义转换器 的定义方法:
// confMapIntInt map[int32]int32
func confMapIntInt(confStr string) (reflect.Value, error) {
fieldValueTmp := make(map[int32]int32)
for _, v := range strings.Split(confStr, "|") {
tArr := strings.Split(v, ":")
if len(tArr) == 2 {
if ret, err := strconv.Atoi(tArr[0]); err == nil {
if ret1, err1 := strconv.ParseInt(tArr[1], 10, 64); err1 == nil {
fieldValueTmp[int32(ret)] = int32(ret1)
}
}
}
}
return reflect.ValueOf(fieldValueTmp), nil
}支持通过 | 作为分隔符,传入多个配置名,从前到后优先级依次降低。
如 conf:"indexSize|macDoc" 配置的意义是:优先取合并后的配置列表中的 indexSize 对应的值,如果没有 indexSize,再取 合并后的配置列表中的 macDoc 对应的值。
如果在调用FillConfByParamsMap 函数时,parseDefault 设置为了 true ,当在传入confMaps 中传入的所有配置列表中都找不到配置值时,会使用该 tag 中的值作为默认配置值进行解析。
conf 支持通过 | 作为分隔符,传入多个配置名,从前到后优先级依次降低。
如 conf:"indexSize|macDoc" 配置的意义是:优先取合并后的配置列表中的 indexSize 对应的值,如果没有 indexSize,再取 合并后的配置列表中的 macDoc 对应的值。
函数签名为 func FillConfByParamsMap(obj **interface{}**, parseDefault **bool**, confMaps **...map[string]string**) **error**
参数:
obj **interface{}:**需要进行配置填充的 struct 的实例化对象指针,FillConfByParamsMap 函数会将配置值填充到该指针的对象上。parseDefault **bool:**是否解析struct tag中defaulttag 中的默认配置值。confMaps **...map[string]string:**配置列表,可传入多组 map[string]string,从前到后优先级依次降低,至少传入一个。传入多组配置列表时,内部会进行合并,优先级低的配置值(靠后的)会被优先级高的配置值(靠前的)覆盖。
反射在带来便利的同时,确实也对性能产生了一些负面影响,不过结合 benchmark 和实际在线上运行效果,其对性能的影响不大,特别是复杂场景下,这部分影响几乎可以忽略不计。
以下为 benchmark 的结果,具体用例见代码:
// go test -bench=. -cpu=1 -benchmem -benchtime=5s
//
// go1.19.4
// goos: darwin
// goarch: arm64
// pkg: github.com/EricLi404/reflectConf
// BenchmarkFillConfByTransformer_Direct 1086484 5549 ns/op 6280 B/op 97 allocs/op
// BenchmarkNoReflect_Direct 26880355 224.6 ns/op 0 B/op 0 allocs/op
// BenchmarkFillConfByTransformer_DirectSlice 600121 10021 ns/op 8432 B/op 198 allocs/op
// BenchmarkNoReflect_DirectSlice 3416010 1727 ns/op 1144 B/op 44 allocs/op
// BenchmarkFillConfByTransformer_DirectMapStruct 632258 9476 ns/op 9928 B/op 188 allocs/op
// BenchmarkNoReflect_DirectMapStruct 2737756 2194 ns/op 2432 B/op 45 allocs/op
// -------
// BenchmarkFillConfByTransformer 1818283 3305 ns/op 3248 B/op 56 allocs/op
// BenchmarkFillConfByHardCode 5407528 1112 ns/op 632 B/op 12 allocs/op手写如 transformer:"direct" conf:"indexSize" default:"100000" 这样的一条配置很容易出错,可以借用 ide 或 编辑器 的功能,实现语法提示。
在 GoLand 中,可以使用 Live Templates 来支持 reflectconf 语法。
Live Templates 功能使用说明见 https://www.jetbrains.com/help/go/settings-live-templates.html
个人 Live Templates 当前配置如下:
在 Go Struct Tag 中 创建如下 配置:
- Templates Text :
`transformer:"$f1$" conf:"$f2$" default:"$END$" `
-
$f1$ :
enum("direct", "diSlice", "diMapStruct", "timeDuration", "confMapIntFloat", "confMapIntInt", "confMapIntString", "confMapIntUintMap")
-
$f2$ :
camelCase(fieldName())
设置生效范围为 Go 。
随着解析器的改变,$f1$ 中的 enum 可能也会改变。
相关截图:
可使用 Snippets 实现类似效果。 参考: https://code.visualstudio.com/docs/editor/userdefinedsnippets

