反对过滤的数据结构
github地址:https://github.com/liu-cn/jso... json-filter
构造体/嵌套构造体/匿名构造体/指针
构造体切片/数组
map
应用场景
有时候你可能会遇到这种状况:一个构造体想要在不同的接口下返回不同的json数据字段。
举个例子:
一个模仿用户的model,实在环境中的字段会要比这个多得多。
type User struct { UID uint `json:"uid"` Avatar string `json:"avatar"` Nickname string `json:"nickname"` Sex int `json:"sex"` VipEndTime time.Time `json:"vip_end_time"` Price string `json:"price"` //上面可能还有更多的字段,实在的环境中可能有几十个字段。}
设计的起因
文章接口中须要返回用户User的数据,然而你只须要在文章接口中返回用户的 UID Avatar 和Nickname这三个字段就够了,并不想暴露出其余用不上的字段,因为返回更多的字段意味着更大的数据,编解码更加耗时,而且也会耗费更多的网络带宽,传输速度会变慢。
这时候你可能须要创立一个新的构造体而后把这三个字段赋值,序列化并返回,
而后业务越来越多,用到User 的中央越来越多,profile接口下须要返回用户的 Nickname VipEndTime 和Price这三个字段,这时候比可能又要再创立一个构造体,再复制字段,赋值,序列化返回,就像这样
传统的解决方案
user:=User{}------------------------------形式1------------------------//定义一个新的model type UserArticleRes struct { UID uint `json:"uid"` Avatar string `json:"avatar"` Nickname string `json:"nickname"` } userRes :=UserArticleRes{ UID: user.UID, Avatar: user.Avatar, Nickname: user.Nickname, } return userRes------------------------------形式2------------------------//或者间接应用匿名构造体。 res:= struct { UID uint `json:"uid"` Avatar string `json:"avatar"` Nickname string `json:"nickname"` }{ UID: user.UID, Avatar: user.Avatar, Nickname: user.Nickname, } return res
方正这两种形式都大差不差,而后更多的场景都用到了User 构造体,私信接口,直播接口,评论等等,而后可能须要反复以上的操作,如果这样搞的话会有很多model须要治理,而且开发效率很低,一个字段改掉其余很多构造体都会受到影响,治理起来麻烦,不灵便,不易扩大,
这时候可能须要用到go的json过滤器来得心应手的复用这一个构造体,得心应手的返回你想要的json数据结构,这时能够用json-filter这个go的开源我的项目
json-filter 地址:https://github.com/liu-cn/jso...
json-filter疾速体验
依照下面的需要试着用json-filter来解决一下看看是怎么解决的。
select选择器
指定字段须要用到的场景
package mainimport ( "fmt" "github.com/liu-cn/json-filter/filter" "time")type User struct { UID uint `json:"uid,select(article)"` //在json的字段名前面写上select(场景0|场景1),括号中能够写多个场景用|宰割 Avatar string `json:"avatar,select(article)"` //Avatar 标记了article的示意article接口(场景)会用到这个字段。 Nickname string `json:"nickname,select(article|profile)"` //Nickname 这里示意article和profile接口(场景)都会用到该字段 Sex int `json:"sex"` //这个没标记用json-filter过滤时不会解析该字段。 VipEndTime time.Time `json:"vip_end_time,select(profile)"` //这个标记了profile 示意只会在profile接口编码应用 Price string `json:"price,select(profile)"` //同上}// NewUser 初始化一个用户构造体,把所有字段都赋值。func NewUser() User { return User{ UID: 0, Avatar: "https://gimg2.baidu.com", Nickname: "昵称", Sex: 1, VipEndTime: time.Now().Add(time.Hour * 24 * 365), Price: "19999.9", }}func main() { //用起来比较简单 articleUser := filter.SelectMarshal("article", NewUser()) //返回值是一个 filter.Filter的构造体 //filter.Filter 罕用的就三个办法 //articleUser.MustJSON() //返回过滤后的json字符串,外部曾经帮忙json.Marshal过了 //如果过程中出错间接panic,Must前缀的办法都是如此。(倡议测试时用这个,不便测试) //articleUser.JSON() //返回过滤后的json字符串和可能产生的谬误 //articleUser.Interface() //返回过滤后待json.Marshal的数据结构,此时还未编码成json能够把此数据间接挂载在待json解析的中央进行解析。 //此时咱们能够间接用MustJSON()打印解析后的json fmt.Println(articleUser.MustJSON()) {"avatar":"https://gimg2.baidu.com","nickname":"昵称","uid":0} //能够看到曾经间接编码成json了,并且只编码了User 构造体tag中select标记中抉择到的字段。 //如果咱们想要编码profile接口下的json数据的话很简略, //因为咱们曾经在构造体tag里把须要的字段曾经标注过了。 //此时间接换个场景,而后间接打印 profileUser := filter.SelectMarshal("profile", NewUser()) fmt.Println(profileUser.MustJSON()) {"nickname":"昵称","price":"19999.9","vip_end_time":"2023-03-08T14:10:15.439302+08:00"} //能够看到输入的json是咱们须要的json数据结构,那些不想要的字段都被过滤掉了。}
如果咱们又减少了新的应用场景,那么咱们只须要在User的构造体标签里增加新的场景就行了不须要反复的创立构造体了,例如咱们新加的私信接口里用到用户,须要Nickname Avatar 和Sex这三个字段的话,看一下怎么扩大。
还得在原来的User构造体上进行操作。
type User struct { UID uint `json:"uid,select(article)"` Avatar string `json:"avatar,select(article|chat)"` //这里增加chat Nickname string `json:"nickname,select(article|profile|chat)"`//这里增加chat Sex int `json:"sex,select(chat)"` //这个也增加chat VipEndTime time.Time `json:"vip_end_time,select(profile)"` Price string `json:"price,select(profile)"` } chatUser := filter.SelectMarshal("chat", NewUser()) fmt.Println(chatUser.MustJSON()) {"avatar":"https://gimg2.baidu.com","nickname":"昵称","sex":1} //很不便就打印出了你想要的构造,如果你还想在这个场景增加一个返回的字段,那再再别的字段也加上这个场景就能够了,很不便。
而后你发现一个问题,就是Nickname这个场景你每个场景都要用到,select()里曾经逐步变得收缩,每个场景都写显得太啰嗦,你不想每增加一个场景就把新的场景也加进去,你心愿有个通用标识符能示意此字段无论任何场景我都要用,
那么很简略,我提供了 $any 标识符用用来标识此字段任何场景都会用到,此时你能够把Nickname字段的标签简化一下由select(article|profile|chat)换成select($any)
咱们能够试一下成果
type User struct { UID uint `json:"uid,select(article)"` Avatar string `json:"avatar,select(article|chat)"` Nickname string `json:"nickname,select($any)"`//这里换成$any 示意所有场景都用 Sex int `json:"sex,select(chat)"` VipEndTime time.Time `json:"vip_end_time,select(profile)"` Price string `json:"price,select(profile)"` } chatUser := filter.SelectMarshal("chat", NewUser()) fmt.Println(chatUser.MustJSON()) {"avatar":"https://gimg2.baidu.com","nickname":"昵称","sex":1} //能够看到仍旧输入了你想要的后果。
这时候又有新的应用场景了,你又发现问题了,这次是一个评论的场景,这次除了Price字段之外其余的所有字段都须要用到,你再去增加新的场景时会发现,好麻烦居然须要把Price字段外的5个字段全副都打上标记,如果构造体字段过多的确会很好受,很不优雅,于是又有了新的选择器
omit选择器
指定场景排除该字段字段,用法和select刚好相同,咱们能够疾速体验一下,并解决下面评论接口的需要
type User struct { UID uint `json:"uid,select(article)"` Avatar string `json:"avatar,select(article|chat)"` Nickname string `json:"nickname,select($any)"` Sex int `json:"sex,select(chat)"` VipEndTime time.Time `json:"vip_end_time,select(profile)"` Price string `json:"price,select(profile),omit(comment)"` //在这里增加omit(comment)选择器间用,宰割。 //此时示意 Price在comment时会被疏忽掉,不会被json解析,这时就省的应用select来标记5个字段了。} //此时须要调用的办法也不同了用select选择器解析须要用SelectMarshal办法,用omit选择器须要用OmitMarshal办法解析, commentUser := filter.OmitMarshal("comment", NewUser()) fmt.Println(commentUser.MustJSON()) {"avatar":"https://gimg2.baidu.com","nickname":"昵称","price":"19999.9","sex":1,"uid":0,"vip_end_time":"2023-03-08T14:47:09.390381+08:00"} //能够看到是本人想要的后果 //OmitMarshal用法大同小异,仍旧能够排除多个字段,仍旧能够用$any示意任意场景都排除该字段, //比方password字段任何场景都不违心裸露就能够用omit($any)来示意任何场景都疏忽解析该字段 //能够参考SelectMarshal的办法来应用OmitMarshal
留神一个问题:
**事实应用中更多是用articleUser.Interface() 这个办法,因为MustJSON()间接编码为json 如果再进行返回序列化的时候会产生反复编码,把json当作字符串编码,能够间接看一个gin的例子。
//封装一下返回的构造func OkWithData(data interface{}, c *gin.Context) { //这里会在进行json编码 c.JSON(200, Response{ Code: 0, Msg: "ok", Data: data, //这个data应该是一个构造体/切片或者map不应该是曾经解析好的json字符串 })}//解决办法func GetUserFilter(c *gin.Context) { OkWithData(filter.SelectMarshal("article", NewUser()).MustJSON(), c)}看一下这样的处理结果是怎么的?{ "Code": 0, "Msg": "ok", "Data": "{"nickname":"boyan","uid":1}"}Data被当字符串编码了,所以想要正确的,被其余的办法编码就须要用Interface()这个办法,他会返回一个过滤后待编码的map,你能够了解就是一个struct,struct是能够被再次编码的,能够看一下成果。
很简略换个办法就能够了func GetUserFilter(c *gin.Context) { OkWithData(filter.SelectMarshal("article", NewUser()).Interface(), c) //换成这样}输入后果,能够看到解析的没故障。{ "Code": 0, "Msg": "ok", "Data": { "nickname": "boyan", "uid": 1 }}
json-filter的高级过滤
对数组和切片的间接过滤
func main() { type Tag struct { ID uint `json:"id,select(all)"` Name string `json:"name,select(justName|all)"` Icon string `json:"icon,select(chat|profile|all)"` } tags := []Tag{ //切片和数组都反对 { ID: 1, Name: "c", Icon: "icon-c", }, { ID: 1, Name: "c++", Icon: "icon-c++", }, { ID: 1, Name: "go", Icon: "icon-go", }, } fmt.Println(filter.SelectMarshal("justName", tags).MustJSON()) //--->输入后果: [{"name":"c"},{"name":"c++"},{"name":"go"}] fmt.Println(filter.SelectMarshal("all", tags).MustJSON()) //--->输入后果: [{"icon":"icon-c","id":1,"name":"c"},{"icon":"icon-c++","id":1,"name":"c++"},{"icon":"icon-go","id":1,"name":"go"}] fmt.Println(filter.SelectMarshal("chat", tags).MustJSON()) //--->输入后果: [{"icon":"icon-c"},{"icon":"icon-c++"},{"icon":"icon-go"}]}
你认为json-filter就这两把刷子?只能过滤这么简略的数据结构吗?
其实不是,只有官网json.Marshal()能解析的,json-filter都能够间接过滤。
咱们来看一个简单的场景 这个Profile构造体能够说是相当简单了,咱们只想疏忽带name的字段,Lang构造体嵌套在Profile的第二层,Art嵌套到第三层,想要层层都过滤一些字段,如果用传统的形式,创立新构造体的话有点让人头皮发麻,此时json-filter仍旧能够在任何简单数据结构帮你间接过滤数据,咱们只增加了两个标记,能够体验一下后果。
type Profile struct { UID uint `json:"uid"` LangAge []Lang `json:"lang_age"`} type Lang struct { Name string `json:"name,omit(omitName)"` //疏忽该字段 Arts []*Art `json:"arts"`} type Art struct { Name string `json:"name,omit(omitName)"` //疏忽该字段 Profile map[string]interface{} `json:"profile"` Values []string `json:"values"`}func NewProfile() Profile { return Profile{ UID: 0, LangAge: []Lang{ { Name: "c++", //这个字段不须要, Arts: []*Art{ { Name: "c++",//这个字段也不须要, Profile: map[string]interface{}{ "c++": "cpp", }, Values: []string{"cpp1", "cpp2"}, }, }, }, { Name: "Go",//这个字段不须要, Arts: []*Art{ { Name: "Golang",//这个字段也不须要, Profile: map[string]interface{}{ "Golang": "go", }, Values: []string{"Golang", "Golang1"}, }, }, }, }, }}omitName := filter.OmitMarshal("omitName", NewProfile())fmt.Println(omitName.MustJSON())//输入后果{"LangAge":[{"Arts":[{"Profile":{"c++":"cpp"},"Values":["cpp1","cpp2"]}]},{"Arts":[{"Profile":{"Golang":"go"},"Values":["Golang","Golang1"]}]}],"UID":0}
丑化一下,能够看到name字段全副被过滤掉了,即便嵌套在深处能够正确解析。
{ "lang_age":[ { "arts":[ { "profile":{ "c++":"cpp" }, "values":[ "cpp1", "cpp2" ] } ] }, { "arts":[ { "profile":{ "Golang":"go" }, "values":[ "Golang", "Golang1" ] } ] } ], "uid":0}
再用select换个场景试一下,这次我只想抉择name字段,其余的都不要,试一下。
这次你须要这么写,因为Lang和Art构造体都是Profile构造体上嵌套的构造体,所以想要Lang构造体的数据就必须标记LangAge为选中状态,同样想要Art中Name字段Arts也要被选中,不然这些父节点间接疏忽掉,他的所有字段(子节点)就不会被解析了,实质上解析时会编码成n叉树结构,所以要想要子节点数据就必须保障父节点被选中,间接看tag就能看懂。
type Profile struct { UID uint `json:"uid"` LangAge []Lang `json:"lang_age,select(justName)"`}type Lang struct { Name string `json:"name,omit(omitName),select(justName)"` Arts []*Art `json:"arts,select(justName)"`}type Art struct { Name string `json:"name,omit(omitName),select(justName)"` //疏忽该字段 Profile map[string]interface{} `json:"profile"` Values []string `json:"values"`} justName := filter.SelectMarshal("justName", NewProfile()) fmt.Println(justName.MustJSON()) {"lang_age":[{"arts":[{"name":"c++"}],"name":"c++"},{"arts":[{"name":"Golang"}],"name":"Go"}]}
json丑化后成果,能够看到这后果正式想要的后果。
{ "lang_age":[ { "arts":[ { "name":"c++" } ], "name":"c++" }, { "arts":[ { "name":"Golang" } ], "name":"Go" } ]}
对于匿名构造体的过滤
type Page struct { PageInfo int `json:"pageInfo,select($any)"` PageNum int `json:"pageNum,select($any)"`}type Article struct { Title string `json:"title,select(article)"` Page `json:",select(article)"` // 这种tag字段名为空的形式会间接把该构造体开展,当作匿名构造体解决 //Page `json:"page,select(article)"` // 留神这里tag里标注了匿名构造体的字段名,所以解析时会解析成对象,不会开展 Author string `json:"author,select(admin)"`}func main() { article := Article{ Title: "c++从研发到脱发", Page: Page{ PageInfo: 999, PageNum: 1, }, } articleJson := filter.SelectMarshal("article", article) fmt.Println(articleJson) //输入后果---> {"pageInfo":999,"pageNum":1,"title":"c++从研发到脱发"}}
如果不想开展Page构造体想变成具名构造体的话很简略
//不想把Page构造体开展也是能够的,很简略,把匿名构造体Page的构造体标签名加上//接下来把Page `json:",select(article)"`换成Page `json:"page,select(article)"`//接下来看一下输入成果,能够看到字段没有被开展{"page":{"pageInfo":999,"pageNum":1},"title":"c++从研发到脱发"}
对于map也是能够间接过滤的,就不演示了,能够本人试一下,写的太多了就没人违心看了,
举荐应用的姿态
type User struct { UID uint `json:"uid,select($any)"` //标记了$any无论抉择任何场景都会解析该参数 Name string `json:"name,select(article|profile|chat)"` Avatar interface{} `json:"data,select(profile|chat)"`}func (u User) ArticleResp() interface{} { //这样当你前面想要优化性能时能够在这里进行优化, return filter.SelectMarshal("article",u).Interface()}func (u User) ProfileResp() interface{} { //这样当你前面想要优化性能时能够在这里进行优化, return filter.SelectMarshal("profile",u).Interface()}func (u User) ChatResp() interface{} { //如果性能呈现瓶颈,想要优化 chat:= struct { UID uint `json:"uid"` Name string `json:"name"` }{ UID: u.UID, Name: u.Name, } return chat}