json-filter 一个golang的json字段过滤器,随心所欲的构造自己的json数据结构,随意复用结构体的开源项目。

1 篇文章 0 订阅
1 篇文章 0 订阅

github地址:https://github.com/liu-cn/json-filter 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/json-filter

json-filter快速体验

按照上面的需求试着用json-filter来解决一下看看是怎么解决的。

select选择器

指定字段需要用到的场景

package main

import (
	"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,你可以理解就是一个structstruct是可以被再次编码的,可以看一下效果。

很简单换个方法就可以了
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())
//输出结果
{"lang_age":[{"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
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可以通过使用反射来实现这个功能。具体步骤如下: 1. 通过输入的结构体名获取该结构体的类型信息: ```go typeInfo := reflect.TypeOf(structName{}) ``` 2. 获取该结构体对应的文件路径: ```go filePath := typeInfo.PkgPath() ``` 3. 使用该路径加载对应的文件: ```go file, err := os.Open(filePath) if err != nil { // 处理文件打开错误 } defer file.Close() ``` 4. 将文件中的数据读取到对应的结构体实例中: ```go structInstance := reflect.New(typeInfo).Interface() decoder := json.NewDecoder(file) err = decoder.Decode(structInstance) if err != nil { // 处理json解码错误 } ``` 5. 将结构体实例断言为对应的结构体类型: ```go resultStruct, ok := structInstance.(structName) if !ok { // 处理类型断言失败 } ``` 完整代码示例: ```go package main import ( "encoding/json" "fmt" "os" "reflect" ) type Person struct { Name string `json:"name"` Age int `json:"age"` } func main() { // 输入的结构体名 structName := "Person" // 获取结构体类型信息 typeInfo := reflect.TypeOf(Person{}) // 获取结构体对应的文件路径 filePath := typeInfo.PkgPath() // 加载文件 file, err := os.Open(filePath) if err != nil { fmt.Println(err) return } defer file.Close() // 将文件中的数据读取到结构体实例中 structInstance := reflect.New(typeInfo).Interface() decoder := json.NewDecoder(file) err = decoder.Decode(structInstance) if err != nil { fmt.Println(err) return } // 将结构体实例断言为对应的结构体类型 resultStruct, ok := structInstance.(Person) if !ok { fmt.Println("类型断言失败") return } fmt.Printf("Name: %s, Age: %d\n", resultStruct.Name, resultStruct.Age) } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值