本文针对 Golang 的结构体字段的打印进行一些研究。其中涉及到一些反射的知识。实际上本文是基于前面积累的反射进行综合使用的一个示例,也在工作中使用着。
问题提出
总结一些实践情况,结构体字段值的输出还是比较常见的,至少笔者目前常用。比如输出某些数据表的数据(代码中会转换为结构体),对比不同版本数据表的数据,对比某些不同版本但格式相同的 json 文件,等。为了优化代码,减少开发维护工作量,需寻找一种高效的方法,打印结构体。初步需求如下:
格式化,目前需迎合 markdown 表格的格式。
接口可通用于数组、map等结构,原则上直接传递某个变量,即可自行输出格式化后的所需内容。
输出方式多样化,如输出到终端或文件。
使用 markdown 是因为笔者需要将输出的数据表内容通过 vuepress 发布到内部 web 服务器上,以便随时查阅。
测试数据
本文使用的测试数据如下:
typeTestObjstruct{NamestringValueuint64Sizeint32Guardfloat32}varobjects[]TestObjobject1:=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)varmyMapmap[string]TestObjmyMap=make(map[string]TestObj)myMap["obj3"]=TestObj{"JimKent",103,201,102.56}myMap["obj1"]=TestObj{"Kent",101,201,102.56}myMap["obj2"]=TestObj{"Kent",102,201,102.56}
效果
对于可识别渲染 markdown 的平台来说,输出的如下结果:
printbyline-slicedefaulttotal:2|Name|Value|Size|Guard||-------------|-----|----|-----||Jim<br>Kent|128|256|56.4||James1|128|259|56.4|
就能正常显示表格形式。如下:
print by line - slice default total: 2
简单版本
遍历结构体数据,并打印之:
fora,b:=rangeobjects{fmt.Printf("%v%v\n",a,b)//fmt.Printf("%v%+v\n",a,b)}
如果需要格式化,需显式给出结构体字段和格式化形式。如下:
fora,b:=rangeobjects{fmt.Printf("%d:%v|%v|%v|%v\n",a,b.Name,b.Value,b.Size,b.Guard)}
以上结果分别如下:
0{Jim|Kent12825656.4}1{James112825956.4}0:Jim|Kent|128|256|56.41:James1|128|259|56.4
由于此版本非吾所用,因此只具大致形式。
可以看到,前者简单,不用理会结构体内容,直接使用%v
即可打印,如需要输出结构体字段名,则用%+v
。但其形式固定的,类似{xx xx xx}
这样。后者使用竖线|
将各字段隔开,需一一写出字段(当然也可忽略部分字段)。
reflect版本
代码如下:
funccheckSkipNames(astring,b[]string)bool{for_,item:=rangeb{ifitem==a{returntrue}}returnfalse}//结构体的字段名称funcGetStructName(myrefreflect.Value,names[]string)(bufferstring){//注:有可能传递string数组,此时没有“标题”一说,返回ifmyref.Type().Name()=="string"{return}fori:=0;i<myref.NumField();i++{ifok:=checkSkipNames(myref.Type().Field(i).Name,names);ok{continue}buffer+=fmt.Sprintf("|%v",myref.Type().Field(i).Name)}buffer+=fmt.Sprintf("|\n")fori:=0;i<myref.NumField();i++{ifok:=checkSkipNames(myref.Type().Field(i).Name,names);ok{continue}buffer+=fmt.Sprintf("|---")}buffer+=fmt.Sprintf("|\n")return}//将|替换为<br>funcreplaceI(textstring)(retstring){//下面2种方法都可以//reg:=regexp.MustCompile(`|`)//ret=reg.ReplaceAllString(text,`${1}<br/>`)ret=strings.Replace(text,"|","<br>",-1)//fmt.Printf("!!!%q\n",ret)returnret}//结构体的值funcGetStructValue(myrefreflect.Value,names[]string)(bufferstring){//注:有可能传递string数组,此时没有“字段”一说,返回原本的内容ifmyref.Type().Name()=="string"{returnmyref.Interface().(string)}fori:=0;i<myref.NumField();i++{ifok:=checkSkipNames(myref.Type().Field(i).Name,names);ok{continue}//判断是否包含|,有则替换,其必须是string类型,其它保持原有的t:=myref.Field(i).Type().Name()ift=="string"{varstrstring=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}funcPrintStructTable(datainterface{},titlestring,skipNames...string){varwio.Writerw=os.Stdout//settostdoutbuffer,num:=PrintStructTable2Buffer(data,title,skipNames...)fmt.Fprintf(w,"total:%v\n",num)fmt.Fprintf(w,"%v\n",buffer)}/*功能:指定结构体data,其可为slicemap单独结构体指定自定义标题,为空则使用结构体字段指定忽略的字段名称(即结构体字段的变量)按结构体定义的顺序列出,如自定义标题,则必须保证一致。*/funcPrintStructTable2Buffer(datainterface{},titlestring,skipNames...string)(bufferstring,numint){buffer=""t:=reflect.TypeOf(data)v:=reflect.ValueOf(data)varskipNamess[]stringfor_,item:=rangeskipNames{skipNamess=append(skipNamess,item)}//打印结构体字段标志innertitle:=falseprintHead:=falseiflen(title)==0{innertitle=true}//不同类型的,其索引方式不同,故一一判断使用switcht.Kind(){casereflect.Slice,reflect.Array:num=v.Len()ifinnertitle{buffer+=GetStructName(v.Index(0),skipNamess)}else{buffer+=fmt.Sprintln(title)}fori:=0;i<v.Len();i++{buffer+=GetStructValue(v.Index(i),skipNamess)}casereflect.Map:num=v.Len()iter:=v.MapRange()foriter.Next(){if!printHead{ifinnertitle{buffer+=GetStructName(iter.Value(),skipNamess)}else{buffer+=fmt.Sprintln(title)}printHead=true}buffer+=GetStructValue(iter.Value(),skipNamess)}default:num=1//单独结构体不能用Len,单独赋值if!printHead{ifinnertitle{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("printbyline-slicedefault")buf,num:=PrintStructTable2Buffer(objects,"")fmt.Println("total:",num)fmt.Println(buf)//数组,自定义标题fmt.Println("printbyline-slice")buf,num=PrintStructTable2Buffer(objects,"|Name|Value|Size|Guard|\n|---|---|---|++++|")fmt.Println("total:",num)fmt.Println(buf)//单个对象fmt.Println("printbyline-singleobject")buf,num=PrintStructTable2Buffer(object1,"|Name|Value|Guard|\n|+++|+++|+++|","Size")fmt.Println("total:",num)fmt.Println(buf)//mapfmt.Println("printbyline-map")buf,num=PrintStructTable2Buffer(myMap,"aaa")fmt.Println(buf)
测试结果如下:
printbyline-slicedefaulttotal:2|Name|Value|Size|Guard||---|---|---|---||Jim<br>Kent|128|256|56.4||James1|128|259|56.4|printbyline-slicetotal:2|Name|Value|Size|Guard||---|---|---|++++||Jim<br>Kent|128|256|56.4||James1|128|259|56.4|printbyline-singleobjecttotal:1|Name|Value|Guard||+++|+++|+++||Jim<br>Kent|128|56.4|printbyline-mapaaa|JimKent|103|201|102.56||Kent|101|201|102.56||Kent|102|201|102.56|
观察结果,可达到预期目的。