Golang实践录:利用反射reflect构建通用打印结构体接口

本文针对 Golang 的结构体字段的打印进行一些研究。其中涉及到一些反射的知识。

问题提出

总结一些实践情况,结构体字段值的输出还是比较常见的,至少笔者目前常用。比如输出某些数据表的数据(代码中会转换为结构体),对比不同版本数据表的数据,对比某些不同版本但格式相同的 json 文件,等。为了优化代码,减少开发维护工作量,需寻找一种高效的方法。打印结构体。需求如下:

  • 格式化,目前需迎合 markdown 表格格式。
  • 接口可通用于数组、map等结构,原则上直接传递某个变量,即可自行输出格式化后的所需内容。
  • 可输出到终端,或文件。

测试数据

本文使用的测试数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type TestObj struct {
Name string
Value uint64
Size int32
Guard float32
}

var objects []TestObj

object1 := TestObj{
Name: "Jim | Kent",
Value: 128,
Size: 256,
Guard: 56.4,
}
object2 := TestObj{
Name: "James1",
Value: 128,
Size: 259,
Guard: 56.4,
}

objects = append(objects, object1)
objects = append(objects, object2)

var myMap map[string]TestObj
myMap = make(map[string]TestObj)
myMap["obj3"] = TestObj{"Jim Kent", 103, 201, 102.56}
myMap["obj1"] = TestObj{"Kent", 101, 201, 102.56}
myMap["obj2"] = TestObj{"Kent", 102, 201, 102.56}

效果

对于可识别渲染 markdown 的平台来说,输出的如下结果:

1
2
3
4
5
6
7
print by line - slice default  
total: 2

| Name | Value | Size | Guard |
| ------------- | ----- | ---- | ----- |
| Jim <br> Kent | 128 | 256 | 56.4 |
| James1 | 128 | 259 | 56.4 |

就能正常显示表格形式。如下:

print by line - slice default
total: 2

Name Value Size Guard
Jim
Kent
128 256 56.4
James1 128 259 56.4

简单版本

遍历结构体数据,并打印之:

1
2
3
4
for a, b := range objects {
fmt.Printf("%v %v\n", a, b)
// fmt.Printf("%v %+v\n", a, b)
}

如果需要格式化,需显式给出结构体字段和格式化形式。如下:

1
2
3
for a, b := range objects {
fmt.Printf("%d: %v | %v | %v | %v\n", a, b.Name, b.Value, b.Size, b.Guard)
}

以上结果分别如下:

1
2
3
4
5
0 {Jim | Kent 128 256 56.4}
1 {James1 128 259 56.4}

0: Jim | Kent | 128 | 256 | 56.4
1: James1 | 128 | 259 | 56.4

由于此版本非吾所用,因此只具大致形式。

可以看到,前者简单,不用理会结构体内容,直接使用%v即可打印,如需要输出结构体字段名,则用%+v。但其形式固定的,类似{xx xx xx}这样。后者使用竖线|将各字段隔开,需一一写出字段(当然也可忽略部分字段)。

reflect版本

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
func checkSkipNames(a string, b []string) bool {
for _, item := range b {
if item == a {
return true
}
}
return false
}

// 结构体的字段名称
func GetStructName(myref reflect.Value, names []string) (buffer string) {
// 注:有可能传递string数组,此时没有“标题”一说,返回
if myref.Type().Name() == "string" {
return
}
for i := 0; i < myref.NumField(); i++ {
if ok := checkSkipNames(myref.Type().Field(i).Name, names); ok {
continue
}
buffer += fmt.Sprintf("| %v ", myref.Type().Field(i).Name)
}
buffer += fmt.Sprintf("|\n")
for i := 0; i < myref.NumField(); i++ {
if ok := checkSkipNames(myref.Type().Field(i).Name, names); ok {
continue
}
buffer += fmt.Sprintf("| --- ")
}
buffer += fmt.Sprintf("|\n")
return
}

// 将 | 替换为 <br>
func replaceI(text string) (ret string) {
// 下面2种方法都可以
// reg := regexp.MustCompile(`\|`)
// ret = reg.ReplaceAllString(text, `${1}<br/>`)
ret = strings.Replace(text, "|", "<br>", -1)
// fmt.Printf("!!! %q\n", ret)
return ret
}

// 结构体的值
func GetStructValue(myref reflect.Value, names []string) (buffer string) {
// 注:有可能传递string数组,此时没有“字段”一说,返回原本的内容
if myref.Type().Name() == "string" {
return myref.Interface().(string)
}

for i := 0; i < myref.NumField(); i++ {
if ok := checkSkipNames(myref.Type().Field(i).Name, names); ok {
continue
}
// 判断是否包含|,有则替换,其必须是string类型,其它保持原有的
t := myref.Field(i).Type().Name()
if t == "string" {
var str string = myref.Field(i).Interface().(string)
str = replaceI(str)
buffer += fmt.Sprintf("| %v ", str)
} else {
buffer += fmt.Sprintf("| %v ", myref.Field(i).Interface())
}
}
buffer += fmt.Sprintf("|\n")

return
}

func PrintStructTable(data interface{}, title string, skipNames ...string) {
var w io.Writer
w = os.Stdout // set to stdout
buffer, num := PrintStructTable2Buffer(data, title, skipNames...)
fmt.Fprintf(w, "total: %v\n", num)
fmt.Fprintf(w, "%v\n", buffer)
}

/*
功能:指定结构体data,其可为slice map 单独结构体
指定自定义标题,为空则使用结构体字段
指定忽略的字段名称(即结构体字段的变量)
按结构体定义的顺序列出,如自定义标题,则必须保证一致。
*/
func PrintStructTable2Buffer(data interface{}, title string, skipNames ...string) (buffer string, num int) {
buffer = ""

t := reflect.TypeOf(data)
v := reflect.ValueOf(data)

var skipNamess []string
for _, item := range skipNames {
skipNamess = append(skipNamess, item)
}

// 打印结构体字段标志
innertitle := false
printHead := false
if len(title) == 0 {
innertitle = true
}

// 不同类型的,其索引方式不同,故一一判断使用
switch t.Kind() {
case reflect.Slice, reflect.Array:
num = v.Len()
if innertitle {
buffer += GetStructName(v.Index(0), skipNamess)
} else {
buffer += fmt.Sprintln(title)
}
for i := 0; i < v.Len(); i++ {
buffer += GetStructValue(v.Index(i), skipNamess)
}
case reflect.Map:
num = v.Len()
iter := v.MapRange()
for iter.Next() {
if !printHead {
if innertitle {
buffer += GetStructName(iter.Value(), skipNamess)
} else {
buffer += fmt.Sprintln(title)
}
printHead = true
}
buffer += GetStructValue(iter.Value(), skipNamess)
}
default:
num = 1 // 单独结构体不能用Len,单独赋值
if !printHead {
if innertitle {
buffer += GetStructName(v, skipNamess)
} else {
buffer += fmt.Sprintln(title)
}
printHead = true
}
buffer += GetStructValue(v, skipNamess)
}

return
}

上述代码提供的对外接口为PrintStructTable2BufferPrintStructTable,因为默认格式为markdown表格形式,故加上Table。前者输出到缓冲区的(可继续写到文件中),后者直接输出终端。真正实现的接口为PrintStructTable2Buffer,其提供了自定义标题,和忽略的字段参数,如果不指定标题,必须将title置为空,因为最后的参数是可变参数,只能有一个,如不写,则输出所有字段。

至于内部实现,因为需要根据用户输入忽略某些字段,因此定义checkSkipNames检查参数,利用GetStructName获取结构体名称,GetStructValue获取结构体的值。不管获取字段还是值,均使用传递的interface{},不需额外传递结构体本身。
注意,由于默认使用竖线分隔,如果字段值本身有竖线,则使用<br>替换——即让该字段的值换行。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 数组,默认形式
fmt.Println("print by line - slice default")
buf, num := PrintStructTable2Buffer(objects, "")
fmt.Println("total:", num)
fmt.Println(buf)

// 数组,自定义标题
fmt.Println("print by line - slice")
buf, num = PrintStructTable2Buffer(objects, "| Name | Value | Size | Guard |\n| --- | --- | --- | ++++ |")
fmt.Println("total:", num)
fmt.Println(buf)
// 单个对象
fmt.Println("print by line - single object")
buf, num = PrintStructTable2Buffer(object1, "| Name | Value | Guard |\n| +++ | +++ | +++ |", "Size")
fmt.Println("total:", num)
fmt.Println(buf)
// map
fmt.Println("print by line - map")
buf, num = PrintStructTable2Buffer(myMap, "aaa")
fmt.Println(buf)

测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
print by line - slice default
total: 2
| Name | Value | Size | Guard |
| --- | --- | --- | --- |
| Jim <br> Kent | 128 | 256 | 56.4 |
| James1 | 128 | 259 | 56.4 |

print by line - slice
total: 2
| Name | Value | Size | Guard |
| --- | --- | --- | ++++ |
| Jim <br> Kent | 128 | 256 | 56.4 |
| James1 | 128 | 259 | 56.4 |

print by line - single object
total: 1
| Name | Value | Guard |
| +++ | +++ | +++ |
| Jim <br> Kent | 128 | 56.4 |

print by line - map
aaa
| Jim Kent | 103 | 201 | 102.56 |
| Kent | 101 | 201 | 102.56 |
| Kent | 102 | 201 | 102.56 |

观察结果,可达到预期目的。