Golang实践录:利用反射reflect构建通用打印结构体接口
Posted 李迟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang实践录:利用反射reflect构建通用打印结构体接口相关的知识,希望对你有一定的参考价值。
本文针对 Golang 的结构体字段的打印进行一些研究。其中涉及到一些反射的知识。
问题提出
总结一些实践情况,结构体字段值的输出还是比较常见的,至于笔者目前常用。比如输出某些数据表的数据(代码中会转换为结构体),对比不同版本数据表的数据,对比某些不同版本但格式相同的 json 文件,等。为了优化代码,减少开发维护工作量,需寻找一种高效的方法。打印结构体。需求如下:
- 格式化,目前需迎合 markdown 表格格式。
- 接口可通用于数组、map等结构,原则上直接传递某个变量,即可自行输出格式化后的所需内容。
- 可输出到终端,或文件。
测试数据
本文使用的测试数据如下:
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 的平台来说,输出的如下结果:
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 |
简单版本
遍历结构体数据,并打印之:
for a, b := range objects {
fmt.Printf("%v %v\\n", a, b)
// fmt.Printf("%v %+v\\n", a, b)
}
如果需要格式化,需显式给出结构体字段和格式化形式。如下:
for a, b := range objects {
fmt.Printf("%d: %v | %v | %v | %v\\n", a, b.Name, b.Value, b.Size, b.Guard)
}
以上结果分别如下:
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版本
代码如下:
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) {
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
}
上述代码提供的对外接口为PrintStructTable2Buffer
和PrintStructTable
,因为默认格式为markdown
表格形式,故加上Table
。前者输出到缓冲区的(可继续写到文件中),后者直接输出终端。真正实现的接口为PrintStructTable2Buffer
,其提供了自定义标题,和忽略的字段参数,如果不指定标题,必须将title
置为空,因为最后的参数是可变参数,只能有一个,如不写,则输出所有字段。
至于内部实现,因为需要根据用户输入忽略某些字段,因此定义checkSkipNames
检查参数,利用GetStructName
获取结构体名称,GetStructValue
获取结构体的值。不管获取字段还是值,均使用传递的interface{}
,不需额外传递结构体本身。
注意,由于默认使用竖线分隔,如果字段值本身有竖线,则使用<br>
替换——即让该字段的值换行。
测试代码如下:
// 数组,默认形式
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)
测试结果如下:
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 |
观察结果,可达到预期目的。
以上是关于Golang实践录:利用反射reflect构建通用打印结构体接口的主要内容,如果未能解决你的问题,请参考以下文章
golang reflect反射(一):interface接口的入门(大白话)
Golang反射机制的实现分析——reflect.Type类型名称