golang json 创建 递归_玩转golang——JSON高性能自动字段名

前言

golang最近在中国非常火爆,尤其是后端服务开发场景。原生并发支持、优秀的性能、统一的风格,极大提升了开发效率。笔者用golang独立开发过不少小中型系统,写了几万行代码,确实很爽。

不过,统一的风格,也带来了一些问题。

从一个久远的争论说起There are only two hard things in Computer Science: cache invalidation and naming things. by Phil Karlton

计算机科学只有两大难题,命名占了一半。

腾讯QQ的程序员喜欢匈牙利命名法,比如szName,stUser,astUserList,bOk。在名字前面加上类型的标记,写起来很有安全感。

linux开发或许最喜欢下划线命名法(GNU编码风格),比如do_linuxrc,release_libc_mem。单词之间有下划线分隔,更易读。

还有人习惯于驼峰命名法,尤其是几年前的前端,因为jQuery全是这样的API,连自己都是。这种风格节约空间,易读性也不错。

到了golang这里,情况就变了。公共字段、函数、方法,都必须使用大写字母开头,为了可读性,基本上只能使用Pascal风格,如ListenAndServe。

笔者在编码时,是比较认可这种风格的。公有自定义类型、方法、函数和结构体字段,使用Pascal风格,私有内容用驼峰式,局部变量用小写,写代码很清爽。

但是在网络协议和数据库存储中,Pascal风格比较难受。一方面,每个字母都大写不符合英语阅读习惯,且英文单词间总是有空格,Pascal过于紧凑,不利于浏览协议、日志和数据

另一方面,在手敲协议或数据库语句时,每个字母都可能出现大写要按shift,主shift手经常同时按两个键。长期写代码的老油条一定都有这种感觉,左手按shift会导致手型变化,可能手腕会旋转,再去按字母键的话,效率比较低,且手腕更易磨损。

用下划线风格的好处,还不止这些。如果数据接入自然语言处理的话,只有下划线风格可以方便地获得关键词

搜索系统同理

在使用文本查找的方式阅览代码或数据库时,通常不区分大小写,其他风格会出现很多跨词结果,造成干扰

……

不仅适合阅读,提升效率,便于扩展,甚至还能避免一些健康风险。

所以,在数据库和网络协议上,下划线命名法才是首选。

那么,用go语言时,如何让struct字段变成下划线风格呢?

原生的JSON字段命名方式

golang在默认情况下,json.Marshal的结果就是字段名,开发者也可以通过json tag来自定义字段名。

type Student struct {

Name string `json:"name"`

MathScore int `json:"math_score"`

StudentNO string `json:"student_no"`

}

这很好,且没有性能损失。只是多写了几个字而已。

对于一个只包含三五个,十个八个struct的系统而言,多写几行代码不成问题。但一个有几十个上百个struct的业务,也要一个一个写过来吗?

就算你敢写,我也不敢用。机械化重复的工作,人力太不可靠。执行的人可能出错,找人检查一样可能出错。几千条配置,还可能继续增加,完全依赖手写?太危险了。

朴素自动化方案

代码生成器

通过“某种方式”,获取代码中的全部结构体,自动生成设置了tag的新代码,再编译。

这种方式运行时效率是最高的,但是真的可行吗?首先,go并未提供直接获取包中所有结构体的原生方法,所以只能自己做代码解析。

其次,并不是所有结构体都是type X struct开头的简单模式。在go中,匿名结构体有很多漂亮的用法,比如快速实现JSON数据的平铺组装。为了适配struct的各种场景,不得不做更深入的解析。

最后,代码生成器作为外部工具,很难管理生效范围。项目依赖外部包是否也要使用此法生成?如何界定哪里应该使用转换,哪里不用?随着项目的膨胀,这将会是一场灾难。

成本高,配置复杂,是其硬伤。

笔者曾使用go-protobuf来部分解决此问题,需要单独管理proto文件,在makefile中处理生成逻辑。后来需要对bson也照此处理,不得不去修改pb源码才支持。虽然省了手写tag,但依然要手写pb。每个新项目还要带着一坨定制环境。

非常难受。

修改JSON包

另一个直观的方式是修改json包。如无tag指定,golang默认使用代码中的字段名,在这里加一个逻辑,变成自己想要的风格,不就行了吗?

当然行了!而且开发成本和运行成本,都非常低!

但还是有几个问题:直接修改GOROOT代码?就掉坑里了。其它引用了json的包,全都受到了影响。

fork一份,只给自己用?当其他格式也需要做转换时,就都要fork一份(不过一共也没几种格式)

如果想要修改bson,那需要将其所属的mgo包也一并带走,不然无法操作数据库。

如果引用了其他包含json/bson/mgo的包,要把这些包通通带走,并把其引用json/bson/mgo的代码改为指向自己的。

如果引用了“引用了上述其他包”的包,要把这些包通通带走,并……

每个引用都要想办法处理,还要考虑引用了那个引用的引用,子子孙孙无穷尽也。写个代码还要发扬一下愚公移山的精神

使用map

开发自己的Marshal函数,先把原始struct marshal一次,再unmarshal成map,再处理map key风格,再用json.Marshal。

这个很爽啊,写几行非常简单的代码,就解决了问题!

func MyMarshal(obj interface{}) (b []byte, e error) {

b, e = json.Marshal(obj)

if e != nil {

return

}

var m map[string]interface{}

e = json.Unmarshal(b, &m)

if e != nil {

return

}

HandleMapStyle(m)

return json.Marshal(m)

}

func HandleMapStyle(m map[string]interface{}) {

for key, value := range m {

switch v := value.(type) {

case []interface{}:

for i := range v {

if elem, ok := v.(map[string]interface{}); ok {

HandleMapStyle(elem)

}

}

case map[string]interface{}:

HandleMapStyle(v)

}

delete(m, key)

m[strings.ToLower(key)] = value //此处简化处理, 全变小写 }

}

写完之后发现,这个功能比想象中稍复杂一点,用了30行左右。但也足够简单了。下次招人的时候,我就先拿这个问题来考,10分钟以内写出来并考虑到一些特殊情况,说明对json包、go类型和递归,都有一些基本掌握。

那么这种方案好不好呢?我相信做过开发的一眼就能看出来,非常差。map丢失了原来struct的信息,无法再自定义字段名。不过这个可以通过在key上打标记来解决。

性能非常差。构造了一个简单struct测试,性能开销是原生方法的16倍。

这就意味着,你开发的服务,原来一台机器就能干的活,现在可能需要加10台。小王啊

优化map方案

上一个方案中,因为做了额外的Marshal和Unmarshal,导致了不必要的开销。那么,如果我直接用reflect构造map,是不是会好一些呢?

会的。

我们直接使用http://github.com/fatih/structs来处理struct to map,MyMarshal改造如下

import "github.com/fatih/structs"

func MyMarshal(obj interface{}) (b []byte, e error) {

m := structs.Map(obj)

HandleMapStyle(m)

return json.Marshal(m)

}

经过实测,性能损耗约12倍。boss还是会找你麻烦。

你被炒了

终极解决方案?

一个合理的方案,必须同时满足性能损耗足够低。至少保证性能跟json.Marshal在同一数量级

保持扩展能力。风格转换只影响默认行为,对于自定义tag,仍然需要支持

易于维护。不污染项目环境,不影响外部依赖

那要怎么做呢?

基本思想

要解析一份数据结构,除了转map去搞,就只要用reflect。

所以,我们要充分利用reflect的能力,给struct的字段加上tag。

那不是很简单?go reflect包提供了StructOf方法,可以随意构造动态类型!拿笔来!

func MyStruct(t reflect.Type) reflect.Type {

if t.Kind() != reflect.Struct {

panic("invalid type")

}

fs := make([]reflect.StructField, t.NumField())

for i := 0; i < t.NumField(); i++ {

f := t.Field(i)

fs[i] = f

// 目前不考虑其他tag if f.Tag.Get("json") == "" {

fs[i].Tag = reflect.StructTag(`json:"` + strings.ToLower(f.Name) + `"`)

}

var ftype reflect.Type

switch f.Type.Kind() {

case reflect.Struct:

ftype = MyStruct(f.Type)

case reflect.Slice:

if f.Type.Elem().Kind() == reflect.Struct {

ftype = reflect.SliceOf(MyStruct(f.Type.Elem()))

} else if f.Type.Elem().Kind() == reflect.Slice {

panic("multi-d slice not supported") //多维数组暂不考虑 }

default: //样例暂中不考虑Ptr/Map/Array等场景, 处理方式类似 ftype = f.Type

}

fs[i].Type = ftype

}

return reflect.StructOf(fs)

}

10分钟再撸一个。测试一下

type Person struct {

Name string

Age int

Avatar struct {

Url string

Height int

Width int

}

}

func main() {

fmt.Println(MyStruct(reflect.TypeOf(Person{})))

}

输出美化后是

struct {

Name string "json:\"name\""

Age int "json:\"age\""

Avatar struct {

Url string "json:\"url\"";

Height int "json:\"height\"";

Width int "json:\"width\""

} "json:\"avatar\""

}

完美,成功设置上了。赶紧发布上线!

秋豆麻袋!

上面这份代码,有可能会触发go语言百年难遇,但程序员几乎全都知道的一个panic。

……

……

……

如果哪位同学看到这里就想到了,请在回复中留言。虽然没有物质奖励,笔者会替大家佩服你一下。

是什么呢?

stack overflow

它曾是C开发者的噩梦,在go里几乎见不到。但是在这里,如果struct定义引用了自己,就会触发栈溢出。

栈溢出

在树或链表定义中经常能见到,节点类型包含了指向自己的指针。用自己定义自己,就是自引用

type Node struct {

V int

Next *Node

}

上述代码因为递归处理每个类型,如果存在自引用,就卡在自己身上出不来了。

不论是直接引用自己,还是隔代引用自己,或是子结构存在自引用,都会栈溢出。

遗憾的是,这个问题碰到了go reflect的天花板:go目前(1.12)没有办法通过reflect定义自引用struct。

怎么办?好不容易才找到正确的道路,就这么夭折了吗?

幸运的是,我们主要面对的场景是网络协议和数据库。事实上,协议和数据库是不会存在无限自引用结构的。不论链表还是树,都会用数组来存储。即便某个业务(或某个有个性的前端)非要用自引用的协议,也不可能是无限层的,现实的业务必然有其上限。

所以我们设定一个合理的上限,在递归中记录同一个struct出现的次数,达到后再出现就不再处理,即可满足实践中所有场景。

使用动态类型

现在我们获得了神奇的动态类型,赶紧写代码试试。

myStruct := MyStruct(Person{})

//然后咋写?

myStruct是个reflect.Type,这要怎么用啊?

这是什么鬼

//一般而言要这么用inst := reflect.New(myStruct)

inst.Elem().FieldByName("Name").SetString("大福加冰")

inst.Elem().FieldByName("Avatar").FieldByName("Height").SetInt(1080)

json.Marshal(inst.Interface())坑爹呢!

动态类型虽然是由静态类型生成的,但本质上不是一个东西,无法直接类型转换。为自引用做了一次限制后,实际上也已经完全不一样了。

难道只能想办法把静态对象的字段值一个个copy到动态类型里?但这样类型检查+copy,性能真的能比map好吗?

世界上最遥远的距离,是动态对象在我面前,我却过不去。

看到这里如果有高性能思路的同学,可以在评论留言,笔者佩服+1

内存解释器

go是开发语言中的新锐,但骨子里流淌着c的血。

一个对象,本质就是一段内存而已。其含义都是类型赋予的。

而类型,其实就是内存的解释器而已。

只要用动态类型去解释静态对象的内存,就可以了!

p := Person{

Name: "大福加冰",

Age: 29,

}

myPerson := MyStruct(p)

dynP := reflect.NewAt(myPerson, unsafe.Pointer(&p))

搞定!

注意:在创建动态类型时,注意保证其与静态类型的格式完全一致。遇到自引用类型终点时,用等长的[]byte来补位即可。

调用方式

上面利用reflect来构造动态类型对象,还是有很多限制的。比如使用转换函数

// 入参src必须是对象指针,不然只能copy一遍对象内存// 此处只考虑对象指针的情况(如非指针, sv.Pointer()会panic)func TypeConvert(src interface{}, dstType reflect.Type) interface{} {

sv := reflect.ValueOf(src)

return reflect.NewAt(dstType, sv.Pointer()).Interface()

}

1. 只有Marshal可以流畅调用

Marshal时可以使用

p := Person{}

json.Marshal(DynamicInstance(p, myStruct))

来获得动态结果。但Unmarshal时,只能传动态对象去接收结果,再转换成静态类型供代码使用。

dp := reflect.New(myStruct)

json.Unmarshal(buffer, dp.Interface())

pIntf := TypeConvert(dp.Interface(), reflect.TypeOf(Person{}))

var p *Person

p = pIntf.(*Person)

//到这里 才能获得原始Person对象, 供代码使用

2. 为了调用流畅性,只能自己封装Marshal/Unmarshal函数。但这样,就失去了扩展性

如果业务要对bson/xml使用此特性,只能自己重写方法。动态类型转换的公共能力,不可能给每种协议格式都专门写一个Marshal/Unmarshal

很难用

终结者unsafe

Too safe, sometimes naive.

reflect还是太safe了。我们要直接用unsafe对内存动手!

import (

. "unsafe"

. "reflect"

)

type emptyInterface struct {

pt Pointer

pv Pointer

}

func PointerOfType(t Type) Pointer {

p := *(*emptyInterface)(Pointer(&t))

return p.pv

}

func TypeCast(src interface{}, dstType Type) (dst interface{}) {

srcType := TypeOf(src)

eface := *(*emptyInterface)(Pointer(&src))

if srcType.Kind() == Ptr {

eface.pt = PointerOfType(PtrTo(dstType))

} else {

eface.pt = PointerOfType(dstType)

}

dst = *(*interface{})(Pointer(&eface))

return

}

上述代码是类型解释的终极杀器:直接解释入参的原始内存,避免了任何copy,Unmarshal可一步到位。

用map记录静态到动态类型的映射,每次操作时查找缓存,将TypeCast加一层快速调用封装,就可以优雅地写代码了!

结果因为动态类型只需创建一次,这个方案本质上只多做了一次map查询和内存解释。几乎没有性能损耗

自定义tag仍然充分支持。

动态类型仅处理入参,对其他引用依赖没有影响。

完美!

后记

golang是非常秩序、优雅的语言。在腾讯,没有历史包袱的很多项目团队,都已经开始尝试用go来实现新业务了。

笔者作为后台开发,曾使用c/c++/python做主开发语言,但现在会用golang来解决所有问题。

有人会认为,语言只是工具,不必太执着。这是完全正确的。

但是,人类社会的每一科技革命,都是工具带来的。火车、马车都是工具,电力、煤炭,也都工具,互联网和书信,也都是工具。好的工具,意味着更高的效率、性能、可维护性……

golang就是新的生产力。

开源

样例

package main

import (

"encoding/json"

"fmt"

. "github.com/dovejb/quicktag"

"reflect"

)

type Person struct {

Name string

Age int

MyChildren []Person

}

func main() {

p := Person{

Name: "dovejb",

Age: 6,

MyChildren: []Person{

Person{

Name: "baby",

Age: 3,

},

},

}

var p2 Person

buf, _ := json.Marshal(Q(p))

fmt.Println(string(buf))

// {"name":"dovejb","age":6,"my_children":[{"name":"baby","age":3,"my_children":null}]}

json.Unmarshal(buf, Q(&p2))

fmt.Println(reflect.DeepEqual(p, p2))

// true}

对quicktag包中全局变量进行修改,可以自定义转换风格和受影响标签

import "github.com/dovejb/quicktag"

import "time"

func init() {

// 自定义转换风格, 默认quicktag.PascalToUnderline, 无omitempty quicktag.StyleConvert = MyStyleConvertFunc // func(string) string // 自定义受影响业务tag, 默认 []string{"json","bson"} quicktag.TagNames = []string{"json", "bson"}

// 自定义自引用最大层级, 默认5 quicktag.MaxSelfRefLevel = 3

// 注意!!! // 如果某类型自己包含了MarshalJSON/UnmarshalJSON等方法,如time.Time,请在字段后手动添加quicktag:"-"来跳过 // 如 data := struct {

ID string `bson:"_id"`

CreatedTime time.Time `quicktag:"-"`

}

// struct中原有的tag, 均会保留}

性能测试

root@dev:/w/try# go test github.com/dovejb/quicktag -bench=.

goos: linux

goarch: amd64

pkg: github.com/dovejb/quicktag

BenchmarkQMarshal-4 1000000 1343 ns/op

BenchmarkJsonMarshal-4 1000000 1565 ns/op

PASS

ok github.com/dovejb/quicktag 3.936s

root@dev:/w/try# go test github.com/dovejb/quicktag -bench=.

goos: linux

goarch: amd64

pkg: github.com/dovejb/quicktag

BenchmarkQMarshal-4 1000000 1635 ns/op

BenchmarkJsonMarshal-4 1000000 2024 ns/op

PASS

ok github.com/dovejb/quicktag 3.714s

QMarshal为什么比原生还快了一点……(多次执行,结果也存在调转的情况,不过此法性能无损是确定的)

欢迎交流,共同进步!

你可能感兴趣的:(golang,json,创建,递归)