go-json技巧
转自
你需要知道的那些go语言json技巧
基本的序列化
首先我们来看一下Go语言中json.Marshal()
(系列化)与json.Unmarshal
(反序列化)的基本用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | type Person struct { Name string Age int64 Weight float64 } func main() { p1 := Person{ Name: "七米" , Age: 18, Weight: 71.5, } // struct -> json string b, err := json.Marshal(p1) if err != nil { fmt.Printf( "json.Marshal failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) // json string -> struct var p2 Person err = json.Unmarshal(b, &p2) if err != nil { fmt.Printf( "json.Unmarshal failed, err:%v\n" , err) return } fmt.Printf( "p2:%#v\n" , p2) } |
输出:
1 2 | str:{ "Name" : "七米" , "Age" :18, "Weight" :71.5} p2:main.Person{Name: "七米" , Age:18, Weight:71.5} |
结构体tag介绍
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
1 | `key1: "value1" key2: "value2" ` |
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
使用json tag指定字段名
序列化与反序列化默认情况下使用结构体的字段名,我们可以通过给结构体字段添加tag来指定json序列化生成的字段名。
1 2 3 4 5 6 | // 使用json tag指定序列化与反序列化时的行为 type Person struct { Name string `json: "name" ` // 指定json序列化/反序列化时使用小写name Age int64 Weight float64 } |
忽略某个字段
如果你想在json序列化/反序列化的时候忽略掉结构体中的某个字段,可以按如下方式在tag中添加-
。
1 2 3 4 5 6 | // 使用json tag指定json序列化与反序列化时的行为 type Person struct { Name string `json: "name" ` // 指定json序列化/反序列化时使用小写name Age int64 Weight float64 `json: "-" ` // 指定json序列化/反序列化时忽略此字段 } |
忽略空值字段
当 struct 中的字段没有值时, json.Marshal()
序列化的时候不会忽略这些字段,而是默认输出字段的类型零值(例如int
和float
类型零值是 0,string
类型零值是""
,对象类型零值是 nil)。如果想要在序列序列化时忽略这些没有值的字段时,可以在对应字段添加omitempty
tag。
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | type User struct { Name string `json: "name" ` Email string `json: "email" ` Hobby []string `json: "hobby" ` } func omitemptyDemo() { u1 := User{ Name: "七米" , } // struct -> json string b, err := json.Marshal(u1) if err != nil { fmt.Printf( "json.Marshal failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) } |
输出结果:
1 | str:{ "name" : "七米" , "email" : "" , "hobby" :null} |
如果想要在最终的序列化结果中去掉空值字段,可以像下面这样定义结构体:
1 2 3 4 5 6 7 | // 在tag中添加omitempty忽略空值 // 注意这里 hobby,omitempty 合起来是json tag值,中间用英文逗号分隔 type User struct { Name string `json: "name" ` Email string `json: "email,omitempty" ` Hobby []string `json: "hobby,omitempty" ` } |
此时,再执行上述的omitemptyDemo
,输出结果如下:
1 | str:{ "name" : "七米" } // 序列化结果中没有email和hobby字段 |
忽略嵌套结构体空值字段
首先来看几种结构体嵌套的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | type User struct { Name string `json: "name" ` Email string `json: "email,omitempty" ` Hobby []string `json: "hobby,omitempty" ` Profile } type Profile struct { Website string `json: "site" ` Slogan string `json: "slogan" ` } func nestedStructDemo() { u1 := User{ Name: "七米" , Hobby: []string{ "足球" , "双色球" }, } b, err := json.Marshal(u1) if err != nil { fmt.Printf( "json.Marshal failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) } |
匿名嵌套Profile
时序列化后的json串为单层的:
1 | str:{ "name" : "七米" , "hobby" :[ "足球" , "双色球" ], "site" : "" , "slogan" : "" } |
想要变成嵌套的json串,需要改为具名嵌套或定义字段tag:
1 2 3 4 5 6 7 | type User struct { Name string `json: "name" ` Email string `json: "email,omitempty" ` Hobby []string `json: "hobby,omitempty" ` Profile `json: "profile" ` } // str:{"name":"七米","hobby":["足球","双色球"],"profile":{"site":"","slogan":""}} |
想要在嵌套的结构体为空值时,忽略该字段,仅添加omitempty
是不够的:
1 2 3 4 5 6 7 | type User struct { Name string `json: "name" ` Email string `json: "email,omitempty" ` Hobby []string `json: "hobby,omitempty" ` Profile `json: "profile,omitempty" ` } // str:{"name":"七米","hobby":["足球","双色球"],"profile":{"site":"","slogan":""}} |
还需要使用嵌套的结构体指针:
1 2 3 4 5 6 7 | type User struct { Name string `json: "name" ` Email string `json: "email,omitempty" ` Hobby []string `json: "hobby,omitempty" ` *Profile `json: "profile,omitempty" ` } // str:{"name":"七米","hobby":["足球","双色球"]} |
不修改原结构体忽略空值字段
我们需要json序列化User
,但是不想把密码也序列化,又不想修改User
结构体,这个时候我们就可以使用创建另外一个结构体PublicUser
匿名嵌套原User
,同时指定Password
字段为匿名结构体指针类型,并添加omitempty
tag,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | type User struct { Name string `json: "name" ` Password string `json: "password" ` } type PublicUser struct { *User // 匿名嵌套 Password * struct {} `json: "password,omitempty" ` } func omitPasswordDemo() { u1 := User{ Name: "七米" , Password: "123456" , } b, err := json.Marshal(PublicUser{User: &u1}) if err != nil { fmt.Printf( "json.Marshal u1 failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) // str:{"name":"七米"} } |
优雅处理字符串格式的数字
有时候,前端在传递来的json数据中可能会使用字符串类型的数字,这个时候可以在结构体tag中添加string
来告诉json包从字符串中解析相应字段的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | type Card struct { ID int64 `json: "id,string" ` // 添加string tag Score float64 `json: "score,string" ` // 添加string tag } func intAndStringDemo() { jsonStr1 := `{ "id" : "1234567" , "score" : "88.50" }` var c1 Card if err := json.Unmarshal([]byte(jsonStr1), &c1); err != nil { fmt.Printf( "json.Unmarsha jsonStr1 failed, err:%v\n" , err) return } fmt.Printf( "c1:%#v\n" , c1) // c1:main.Card{ID:1234567, Score:88.5} } |
整数变浮点数
在 JSON 协议中是没有整型和浮点型之分的,它们统称为number。json字符串中的数字经过Go语言中的json包反序列化之后都会成为float64
类型。下面的代码便演示了这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | func jsonDemo() { // map[string]interface{} -> json string var m = make( map [string] interface {}, 1) m[ "count" ] = 1 // int b, err := json.Marshal(m) if err != nil { fmt.Printf( "marshal failed, err:%v\n" , err) } fmt.Printf( "str:%#v\n" , string(b)) // json string -> map[string]interface{} var m2 map [string] interface {} err = json.Unmarshal(b, &m2) if err != nil { fmt.Printf( "unmarshal failed, err:%v\n" , err) return } fmt.Printf( "value:%v\n" , m2[ "count" ]) // 1 fmt.Printf( "type:%T\n" , m2[ "count" ]) // float64 } |
这种场景下如果想更合理的处理数字就需要使用decoder
去反序列化,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | func decoderDemo() { // map[string]interface{} -> json string var m = make( map [string] interface {}, 1) m[ "count" ] = 1 // int b, err := json.Marshal(m) if err != nil { fmt.Printf( "marshal failed, err:%v\n" , err) } fmt.Printf( "str:%#v\n" , string(b)) // json string -> map[string]interface{} var m2 map [string] interface {} // 使用decoder方式反序列化,指定使用number类型 decoder := json.NewDecoder(bytes.NewReader(b)) decoder.UseNumber() err = decoder.Decode(&m2) if err != nil { fmt.Printf( "unmarshal failed, err:%v\n" , err) return } fmt.Printf( "value:%v\n" , m2[ "count" ]) // 1 fmt.Printf( "type:%T\n" , m2[ "count" ]) // json.Number // 将m2["count"]转为json.Number之后调用Int64()方法获得int64类型的值 count, err := m2[ "count" ].(json.Number).Int64() if err != nil { fmt.Printf( "parse to int64 failed, err:%v\n" , err) return } fmt.Printf( "type:%T\n" , int(count)) // int } |
json.Number
的源码定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // A Number represents a JSON number literal. type Number string // String returns the literal text of the number. func (n Number) String() string { return string(n) } // Float64 returns the number as a float64. func (n Number) Float64() (float64, error) { return strconv.ParseFloat(string(n), 64) } // Int64 returns the number as an int64. func (n Number) Int64() (int64, error) { return strconv.ParseInt(string(n), 10, 64) } |
我们在处理number类型的json字段时需要先得到json.Number
类型,然后根据该字段的实际类型调用Float64()
或Int64()
。
自定义解析时间字段
Go语言内置的 json 包使用 RFC3339
标准中定义的时间格式,对我们序列化时间字段的时候有很多限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | type Post struct { CreateTime time.Time `json: "create_time" ` } func timeFieldDemo() { p1 := Post{CreateTime: time.Now()} b, err := json.Marshal(p1) if err != nil { fmt.Printf( "json.Marshal p1 failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) jsonStr := `{ "create_time" : "2020-04-05 12:25:42" }` var p2 Post if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil { fmt.Printf( "json.Unmarshal failed, err:%v\n" , err) return } fmt.Printf( "p2:%#v\n" , p2) } |
上面的代码输出结果如下:
1 2 | str:{ "create_time" : "2020-04-05T12:28:06.799214+08:00" } json.Unmarshal failed, err:parsing time "" 2020-04-05 12:25:42 "" as "" 2006-01-02T15:04:05Z07:00 "" : cannot parse " 12:25:42" " as " T" |
也就是内置的json包不识别我们常用的字符串时间格式,如2020-04-05 12:25:42
。
不过我们通过实现 json.Marshaler
/json.Unmarshaler
接口实现自定义的事件格式解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | type CustomTime struct { time.Time } const ctLayout = "2006-01-02 15:04:05" var nilTime = (time.Time{}).UnixNano() func (ct *CustomTime) UnmarshalJSON(b []byte) (err error) { s := strings.Trim(string(b), "\"" ) if s == "null" { ct.Time = time.Time{} return } ct.Time, err = time.Parse(ctLayout, s) return } func (ct *CustomTime) MarshalJSON() ([]byte, error) { if ct.Time.UnixNano() == nilTime { return []byte( "null" ), nil } return []byte(fmt.Sprintf( "\"%s\"" , ct.Time.Format(ctLayout))), nil } func (ct *CustomTime) IsSet() bool { return ct.UnixNano() != nilTime } type Post struct { CreateTime CustomTime `json: "create_time" ` } func timeFieldDemo() { p1 := Post{CreateTime: CustomTime{time.Now()}} b, err := json.Marshal(p1) if err != nil { fmt.Printf( "json.Marshal p1 failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) jsonStr := `{ "create_time" : "2020-04-05 12:25:42" }` var p2 Post if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil { fmt.Printf( "json.Unmarshal failed, err:%v\n" , err) return } fmt.Printf( "p2:%#v\n" , p2) } |
自定义MarshalJSON和UnmarshalJSON方法
上面那种自定义类型的方法稍显啰嗦了一点,下面来看一种相对便捷的方法。
首先你需要知道的是,如果你能够为某个类型实现了MarshalJSON()([]byte, error)
和UnmarshalJSON(b []byte) error
方法,那么这个类型在序列化(MarshalJSON)/反序列化(UnmarshalJSON)时就会使用你定制的相应方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | type Order struct { ID int `json: "id" ` Title string `json: "title" ` CreatedTime time.Time `json: "created_time" ` } const layout = "2006-01-02 15:04:05" // MarshalJSON 为Order类型实现自定义的MarshalJSON方法 func (o *Order) MarshalJSON() ([]byte, error) { type TempOrder Order // 定义与Order字段一致的新类型 return json.Marshal( struct { CreatedTime string `json: "created_time" ` *TempOrder // 避免直接嵌套Order进入死循环 }{ CreatedTime: o.CreatedTime.Format(layout), TempOrder: (*TempOrder)(o), }) } // UnmarshalJSON 为Order类型实现自定义的UnmarshalJSON方法 func (o *Order) UnmarshalJSON(data []byte) error { type TempOrder Order // 定义与Order字段一致的新类型 ot := struct { CreatedTime string `json: "created_time" ` *TempOrder // 避免直接嵌套Order进入死循环 }{ TempOrder: (*TempOrder)(o), } if err := json.Unmarshal(data, &ot); err != nil { return err } var err error o.CreatedTime, err = time.Parse(layout, ot.CreatedTime) if err != nil { return err } return nil } // 自定义序列化方法 func customMethodDemo() { o1 := Order{ ID: 123456, Title: "《七米的Go学习笔记》" , CreatedTime: time.Now(), } // 通过自定义的MarshalJSON方法实现struct -> json string b, err := json.Marshal(&o1) if err != nil { fmt.Printf( "json.Marshal o1 failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) // 通过自定义的UnmarshalJSON方法实现json string -> struct jsonStr := `{ "created_time" : "2020-04-05 10:18:20" , "id" :123456, "title" : "《七米的Go学习笔记》" }` var o2 Order if err := json.Unmarshal([]byte(jsonStr), &o2); err != nil { fmt.Printf( "json.Unmarshal failed, err:%v\n" , err) return } fmt.Printf( "o2:%#v\n" , o2) } |
输出结果:
1 2 | str:{ "created_time" : "2020-04-05 10:32:20" , "id" :123456, "title" : "《七米的Go学习笔记》" } o2:main.Order{ID:123456, Title: "《七米的Go学习笔记》" , CreatedTime:time.Time{wall:0x0, ext:63721678700, loc:(*time.Location)(nil)}} |
使用匿名结构体添加字段
使用内嵌结构体能够扩展结构体的字段,但有时候我们没有必要单独定义新的结构体,可以使用匿名结构体简化操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | type UserInfo struct { ID int `json: "id" ` Name string `json: "name" ` } func anonymousStructDemo() { u1 := UserInfo{ ID: 123456, Name: "七米" , } // 使用匿名结构体内嵌User并添加额外字段Token b, err := json.Marshal( struct { *UserInfo Token string `json: "token" ` }{ &u1, "91je3a4s72d1da96h" , }) if err != nil { fmt.Printf( "json.Marsha failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) // str:{"id":123456,"name":"七米","token":"91je3a4s72d1da96h"} } |
使用匿名结构体组合多个结构体
同理,也可以使用匿名结构体来组合多个结构体来序列化与反序列化数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | type Comment struct { Content string } type Image struct { Title string `json: "title" ` URL string `json: "url" ` } func anonymousStructDemo2() { c1 := Comment{ Content: "永远不要高估自己" , } i1 := Image{ Title: "赞赏码" , URL: "https://www.liwenzhou.com/images/zanshang_qr.jpg" , } // struct -> json string b, err := json.Marshal( struct { *Comment *Image }{&c1, &i1}) if err != nil { fmt.Printf( "json.Marshal failed, err:%v\n" , err) return } fmt.Printf( "str:%s\n" , b) // json string -> struct jsonStr := `{ "Content" : "永远不要高估自己" , "title" : "赞赏码" , "url" : "https://www.liwenzhou.com/images/zanshang_qr.jpg" }` var ( c2 Comment i2 Image ) if err := json.Unmarshal([]byte(jsonStr), & struct { *Comment *Image }{&c2, &i2}); err != nil { fmt.Printf( "json.Unmarshal failed, err:%v\n" , err) return } fmt.Printf( "c2:%#v i2:%#v\n" , c2, i2) } |
输出:
1 2 | str:{ "Content" : "永远不要高估自己" , "title" : "赞赏码" , "url" : "https://www.liwenzhou.com/images/zanshang_qr.jpg" } c2:main.Comment{Content: "永远不要高估自己" } i2:main.Image{Title: "赞赏码" , URL: "https://www.liwenzhou.com/images/zanshang_qr.jpg" } |
处理不确定层级的json
如果json串没有固定的格式导致不好定义与其相对应的结构体时,我们可以使用json.RawMessage
原始字节数据保存下来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | type sendMsg struct { User string `json: "user" ` Msg string `json: "msg" ` } func rawMessageDemo() { jsonStr := `{ "sendMsg" :{ "user" : "q1mi" , "msg" : "永远不要高估自己" }, "say" : "Hello" }` // 定义一个map,value类型为json.RawMessage,方便后续更灵活地处理 var data map [string]json.RawMessage if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { fmt.Printf( "json.Unmarshal jsonStr failed, err:%v\n" , err) return } var msg sendMsg if err := json.Unmarshal(data[ "sendMsg" ], &msg); err != nil { fmt.Printf( "json.Unmarshal failed, err:%v\n" , err) return } fmt.Printf( "msg:%#v\n" , msg) // msg:main.sendMsg{User:"q1mi", Msg:"永远不要高估自己"} } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)