这几天收到一个战术性需求,将一大坨字段序列化为特定格式的字符串。
大概是下表:
序号
|
字段名
|
描述
|
是否必填
|
0
|
logVersion
|
日志版本
|
是
|
1
|
productName
|
产品
|
是
|
2
|
serviceName
|
服务
|
是
|
...
|
|
|
|
...
|
|
|
|
...
|
|
|
|
25
|
extend3
|
扩展字段3
|
否
|
26
|
extend4
|
扩展字段3
|
否
|
27
|
extend5
|
扩展字段3
|
否
|
控制点1 : 必填字段少,若可选字段无值,则该字段序列化为字符串"null"。
控制点2: 序列化时字段有序,字段值之间用空格区分。
真要我挨个字段填充,我眼睛都要对花, 而且很容易漏掉字段。
// 伪代码如下:
b := bytes.Buffer{}
b.WriteString("P1")
b.WriteString(" ")
b.WriteString("null")
b.WriteString(" ")
b.WriteString("null")
b.WriteString(" ")
b.WriteString("A")
...
b.WriteString(" ")
b.WriteString("null")
log.Info(b.String())
根据"必填字段极少,可选字段默认设为null字符串"的背景,我考虑
1. 使用struct来定义结构,便于对必填字段赋值 (这个行为肉眼友好)
2. 将struct的[字段:字段值]转换为排好序的map键值对(并修正默认值)
3. 对排好序的map键值对无脑序列化

将结构体转换为 map, 这个行为涉及元类型的变动,联想到反射。自古以来,反射也是兵家必争之地, 于是首次操刀golang的反射特性。思路和伪代码很明确,实操时还是有2点障碍:
1. golang付map做for循环,键值对的出现是随机的。
2. 函数传参注意传指针值,而不要传结构体值。
关于第一个问题,利用网上的[提取key放在slice里面,再根据key的排序取map值]的思路是想当然了。我们的key是字符串,sort.Strings()之后keys的排序依旧不是自己的预期(预期是按照struct字段出现的先后顺序)。所以对map做for循环时,能拿到与struct字段出现顺序一致的键值对就是关键。
取巧:
我们利用反射struct时的字段顺序,定义了一个按照struct字段出现顺序为键的map[int]string,这样sort.Ints(keys) 排序之后,for keys能拿到我们想要的键值对顺序。
func constructFixedMap(body interface{}) map[int]string {
// 堆代码 duidaima.com
typ := reflect.TypeOf(body) //TypeOf返回目标数据类型
val := reflect.ValueOf(body) //ValueOf返回目标数据的的值
if typ.Kind() != reflect.Pointer {
fmt.Println("expect pointer")
return nil
}
typ = typ.Elem() // 返回指针所指向的原值
val = val.Elem()
mp := make(map[int]string, 20)
for i := 0; i < typ.NumField(); i++ {
if typ.Field(i).Type.Kind().String() == "string" {
if val.Field(i).String() == "" { // 可选字段,在反射时被修改
mp[i] = "null"
} else {
mp[i] = val.Field(i).String() // 必填字段,保持不变
}
} else {
if val.Field(i).CanInt() {
mp[i] = strconv.FormatInt(val.Field(i).Int(), 10)
} else {
mp[i] = "null"
}
}
}
return mp
}
记忆点回顾
• golang 反射的运用
• 对map做for循环,键值对的出现是随机的
提示:对keys排序,根据排序的keys再取map键值对可随机应变。