Go语言中JSON处理技巧总结

2023-08-28 14:51:44 浏览数 (1)

大家好,我是渔夫子。

本文总结了go语言中对JSON数据结构和结构体之间相互转换问题及解决方法。

基础使用

使用Go标准库中的 json.Marshal()json.Unmarshal进行基本的序列化和反序列化。

代码语言:javascript复制
type Person struct {
 Name   string
 Age    int64
 Weight float64
}

func main() {
 p1 := Person{
  Name:   "Go学堂",
  Age:    18,
  Weight: 71.5,
 }
 // 将结构体转换成json串
 b, _ := json.Marshal(p1)

 fmt.Printf("str:%sn", b)
    
 // 将json串转换成结构体
 var p2 Person
 json.Unmarshal(b, &p2)

 fmt.Printf("p2:%#vn", p2)
}

输出:

代码语言:javascript复制
str:{"Name":"Go学堂","Age":18,"Weight":71.5}

p2:main.Person{Name:"Go学堂", Age:18, Weight:71.5}

给结构体指定tag属性

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对**反引号 ****``**包裹起来,具体的格式如Name字段:

代码语言:javascript复制
type Person struct {
 Name   string `json:"name"`
 Age    int64
 Weight float64
}

这里的json:"name"就是给Name字段的设置的tag。

tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。如下:

代码语言:javascript复制
type Person struct {
 Name   string `json:"name" param:"name"`
 Age    int64
 Weight float64
}

使用json tag指定字段名

序列化与反序列化默认情况下使用结构体的字段名,我们可以通过给结构体字段添加tag来指定json序列化生成的字段名

代码语言:javascript复制
// 使用json tag指定序列化与反序列化时的行为
type Person struct {
 Name   string `json:"name"` // 指定json序列化/反序列化时使用小写name
 Age    int64
 Weight float64
}

忽略某个字段

如果你想在json序列化/反序列化的时候忽略掉结构体中的某个字段,可以按如下方式在tag中添加**-**。如下Person中的Weight字段

代码语言:javascript复制
// 使用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)。

如果想要在序列序列化时忽略这些没有值的字段时,可以在对应字段添加omitemptytag。

将空值输出的例子

下面是将EmailHobby字段的空值输出的例子:

代码语言:javascript复制
type User struct {
 Name  string   `json:"name"`
 Email string   `json:"email"`
 Hobby []string `json:"hobby"`
}

func omitemptyDemo() {
 u1 := User{
  Name: "Go学堂",
 }
 // struct -> json string
 b, _ := json.Marshal(u1)

 fmt.Printf("str:%sn", b)
}

输出结果:

代码语言:javascript复制
str:{"name":"Go学堂","email":"","hobby":null}

将空值忽略的例子

如果想要在最终的序列化结果中去掉空值字段,可以像下面这样定义结构体,在Email和Hobby的tag中添加omitempty,以表示若字段值为零值,则在序列化时忽略该字段

代码语言:javascript复制
// 在tag中添加omitempty忽略空值
// 注意这里 hobby,omitempty 合起来是json tag值,中间用英文逗号分隔
type User struct {
 Name  string   `json:"name"`
 Email string   `json:"email,omitempty"`
 Hobby []string `json:"hobby,omitempty"`
}

此时,再执行上述的程序,输出结果如下:

代码语言:javascript复制
str:{"name":"Go学堂"} // 序列化结果中没有email和hobby字段

忽略嵌套结构体空值字段

结构体嵌套可分匿名结构体嵌套具名嵌套。这两种方式在进行json序列化时的行为会有所不同。下面通过示例来说明。

匿名嵌套

匿名嵌套是指在结构体中不指定字段名,只指定类型的字段。匿名嵌套在json序列化时,会直接输出类型对应的字段。如下

代码语言:javascript复制
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:  "Go学堂",
  Hobby: []string{"golang", "rust"},
 }
 b, _ := json.Marshal(u1)

 fmt.Printf("str:%sn", b)
}

匿名嵌套Profile时序列化后的json串为单层的:

代码语言:javascript复制
str:{"name":"Go学堂","hobby":["golang","rust"],"site":"","slogan":""}

具名嵌套

想要变成嵌套的json串,需要改为具名嵌套或定义字段tag

代码语言:javascript复制
type User struct {
 Name    string   `json:"name"`
 Email   string   `json:"email,omitempty"`
 Hobby   []string `json:"hobby,omitempty"`
 Profile `json:"profile"`
}
// str:{"name":"Go学堂","hobby":["golang","rust"],"profile":{"site":"","slogan":""}}

想要在嵌套的结构体为空值时,忽略该字段,仅添加omitempty是不够的

代码语言:javascript复制
type User struct {
 Name     string   `json:"name"`
 Email    string   `json:"email,omitempty"`
 Hobby    []string `json:"hobby,omitempty"`
 Profile `json:"profile,omitempty"`
}
// str:{"name":"Go学堂","hobby":["golang","rust"],"profile":{"site":"","slogan":""}}

还需要使用嵌套的结构体指针

代码语言:javascript复制
type User struct {
 Name     string   `json:"name"`
 Email    string   `json:"email,omitempty"`
 Hobby    []string `json:"hobby,omitempty"`
 *Profile `json:"profile,omitempty"`
}
// str:{"name":"Go学堂","hobby":["golang","rust"]}

不修改原结构体,忽略空值字段

我们需要json序列化User,但是不想把密码也序列化,又不想修改User结构体,这个时候我们就可以使用创建另外一个结构体PublicUser匿名嵌套原User同时指定Password字段为匿名结构体指针类型,并添加omitemptytag,示例代码如下:

代码语言:javascript复制
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:     "Go学堂",
  Password: "123456",
 }
 b, _ := json.Marshal(PublicUser{User: &u1})

 fmt.Printf("str:%sn", b)  // str:{"name":"Go学堂"}
}

优雅处理字符串格式的数字

有时候,前端在传递来的json数据中可能会使用字符串类型的数字,这个时候可以在结构体tag中添加string来告诉json包从字符串中解析相应字段的数据

代码语言:javascript复制
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:%vn", err)
  return
 }
 fmt.Printf("c1:%#vn", c1) // c1:main.Card{ID:1234567, Score:88.5}
}

整数变浮点数

在 JSON 协议中是没有整型和浮点型之分的,它们统称为number。json字符串中的数字经过Go语言中的json包反序列化之后都会成为float64类型。下面的代码便演示了这个问题:

代码语言:javascript复制
func jsonDemo() {
 // map[string]interface{} -> json string
 var m = make(map[string]interface{}, 1)
 m["count"] = 1 // int
 b, _ := json.Marshal(m)

 fmt.Printf("str:%#vn", string(b))
    
 // json string -> map[string]interface{}
 var m2 map[string]interface{}
 json.Unmarshal(b, &m2)

 fmt.Printf("value:%vn", m2["count"]) // 1
 fmt.Printf("type:%Tn", m2["count"])  // float64
}

你看,原本m["count"]的值是整型1,但经过序列化和再反序列化后就变成了float64类型了。

这种场景下如果想更合理的处理数字就需要使用decoder去反序列化,示例代码如下:

代码语言:javascript复制
func decoderDemo() {
 // map[string]interface{} -> json string
 var m = make(map[string]interface{}, 1)
 m["count"] = 1 // int
 b, _ := json.Marshal(m)

 fmt.Printf("str:%#vn", string(b))
 // json string -> map[string]interface{}
 var m2 map[string]interface{}
 // 使用decoder方式反序列化,指定使用number类型
 decoder := json.NewDecoder(bytes.NewReader(b))
 decoder.UseNumber()
 decoder.Decode(&m2)

 fmt.Printf("value:%vn", m2["count"]) // 1
 fmt.Printf("type:%Tn", m2["count"])  // json.Number
    
 // 将m2["count"]转为json.Number之后调用Int64()方法获得int64类型的值
 count, _ := m2["count"].(json.Number).Int64()

 fmt.Printf("type:%Tn", int(count)) // int
}

json.Number的源码定义如下:

代码语言:javascript复制
// 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 标准中定义的时间格式,对我们序列化时间字段的时候有很多限制。

代码语言:javascript复制
type Post struct {
 CreateTime time.Time `json:"create_time"`
}

func timeFieldDemo() {
 p1 := Post{CreateTime: time.Now()}
 b, _ := json.Marshal(p1) //这里会输出RFC3339格式的时间

 fmt.Printf("str:%sn", 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:%vn", err)
  return
 }
 fmt.Printf("p2:%#vn", p2)
}

上面的代码输出结果如下:

代码语言:javascript复制
str:{"create_time":"2023-06-01T09:28:06.799214 08:00"}
json.Unmarshal failed, err:parsing time ""2023-06-01 09:28:06"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 12:25:42"" as "T"

也就是内置的json包不识别我们常用的字符串时间格式,如2023-06-01 12:25:42。 不过我们通过实现 json.Marshaler/json.Unmarshaler 接口来实现自定义的事件格式解析。 如下,CustomTime类型实现了json的接口。

代码语言:javascript复制
type CustomTime struct {
 time.Time
}

const ctLayout = "2006-01-02 15:04:05"

var nilTime = (time.Time{}).UnixNano()

// 实现了json.Unmarshaler接口中的方法
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
}

// 实现了json.Marshaler接口中的方法
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:%vn", err)
  return
 }
 fmt.Printf("str:%sn", 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:%vn", err)
  return
 }
 fmt.Printf("p2:%#vn", p2)
}

自定义MarshalJSON和UnmarshalJSON方法

上面那种自定义类型的方法稍显啰嗦了一点,下面来看一种相对便捷的方法。 首先你需要知道的是,如果你能够为某个类型实现了MarshalJSON()([]byte, error)UnmarshalJSON(b []byte) error方法,那么这个类型在序列化(MarshalJSON)/反序列化(UnmarshalJSON)时就会使用你定制的相应方法。

代码语言:javascript复制
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学堂的Golang学习之旅》",
  CreatedTime: time.Now(),
 }
 // 通过自定义的MarshalJSON方法实现struct -> json string
 b, _ := json.Marshal(&o1)

 fmt.Printf("str:%sn", b)
 // 通过自定义的UnmarshalJSON方法实现json string -> struct
 jsonStr := `{"created_time":"2020-04-05 10:18:20","id":123456,"title":"《Go学堂的Golang学习之旅》"}`
 var o2 Order
 if err := json.Unmarshal([]byte(jsonStr), &o2); err != nil {
  fmt.Printf("json.Unmarshal failed, err:%vn", err)
  return
 }
 fmt.Printf("o2:%#vn", o2)
}

输出结果:

代码语言:javascript复制
str:{"created_time":"2020-04-05 10:32:20","id":123456,"title":"《Go学堂的Golang学习之旅》"}
o2:main.Order{ID:123456, Title:"《Go学堂的Golang学习之旅》", CreatedTime:time.Time{wall:0x0, ext:63721678700, loc:(*time.Location)(nil)}}

使用匿名结构体添加字段

使用内嵌结构体能够扩展结构体的字段,但有时候我们没有必要单独定义新的结构体,可以使用匿名结构体简化操作

代码语言:javascript复制
type UserInfo struct {
 ID   int    `json:"id"`
 Name string `json:"name"`
}

func anonymousStructDemo() {
 u1 := UserInfo{
  ID:   123456,
  Name: "Go学堂",
 }
 // 使用匿名结构体内嵌User并添加额外字段Token
 b, err := json.Marshal(struct {
  *UserInfo
  Token string `json:"token"`
 }{
  &u1,
  "91je3a4s72d1da96h",
 })
 if err != nil {
  fmt.Printf("json.Marsha failed, err:%vn", err)
  return
 }
 fmt.Printf("str:%sn", b)
 // str:{"id":123456,"name":"Go学堂","token":"91je3a4s72d1da96h"}
}

使用匿名结构体组合多个结构体

同理,也可以使用匿名结构体来组合多个结构体来序列化与反序列化数据:

代码语言:javascript复制
type Comment struct {
 Content string
}

type Image struct {
 Title string `json:"title"`
 URL   string `json:"url"`
}

func anonymousStructDemo2() {
 c1 := Comment{
  Content: "来学编程呀",
 }
 i1 := Image{
  Title: "Go学堂",
  URL:   "https://goxuetang.github.io",
 }
 // struct -> json string
 b, _ := json.Marshal(struct {
  *Comment
  *Image
 }{&c1, &i1})

 fmt.Printf("str:%sn", b)
 // json string -> struct
 jsonStr := `{"Content":"来学编程呀","title":"Go学堂","url":"https://goxuetang.github.io"}`
 var (
  c2 Comment
  i2 Image
 )
 if err := json.Unmarshal([]byte(jsonStr), &struct {
  *Comment
  *Image
 }{&c2, &i2}); err != nil {
  fmt.Printf("json.Unmarshal failed, err:%vn", err)
  return
 }
 fmt.Printf("c2:%#v i2:%#vn", c2, i2)
}

输出:

代码语言:javascript复制
str:{"Content":"来学编程呀","title":"Go学堂","url":"https://goxuetang.github.io"}
c2:main.Comment{Content:"来学编程呀"} i2:main.Image{Title:"Go学堂", URL:"https://goxuetang.github.io"}

处理不确定层级的json

如果json串没有固定的格式导致不好定义与其相对应的结构体时,我们可以使用json.RawMessage原始字节数据保存下来。

代码语言:javascript复制
type sendMsg struct {
 User string `json:"user"`
 Msg  string `json:"msg"`
}

func rawMessageDemo() {
 jsonStr := `{"sendMsg":{"user":"Go学堂","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:%vn", err)
  return
 }
 var msg sendMsg
 if err := json.Unmarshal(data["sendMsg"], &msg); err != nil {
  fmt.Printf("json.Unmarshal failed, err:%vn", err)
  return
 }
 fmt.Printf("msg:%#vn", msg)
 // msg:main.sendMsg{User:"Go学堂", Msg:"来学编程呀"}
}

总结

本文总结了Go语言在结构体和json串之间相互之间进行转换时的一些技巧。同时,这些技巧也是研发者在实际项目中需要注意的地方,希望本文对你有所帮助。

特别说明:你的关注,是我写下去的最大动力。点击下方公众号卡片,直接关注。关注送《100个go常见的错误》pdf文档、经典go学习资料。

0 人点赞