Go 定制JSON序列化

Posted 半塘少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 定制JSON序列化相关的知识,希望对你有一定的参考价值。

通过使用结构体标签、添加空白和封装响应数据,我们已经能够为JSON响应添加大量定制信息。但是,当这些内容还不够时,您需要更自由地定制JSON时,会发生什么呢?

要回答这个问题,我们首先需要谈谈Go如何处理JSON序列化的一些理论。要理解的关键是:

Go是在什么时候将特殊类型序列化为JSON,它首先查看对应的类型是否实现了MarshalJSON()方法。如果实现了,GO将调用这个方法来决定JSON编码格式。

这么讲有点模糊,我们更精确点。严格地说,当Go将特定类型编码为JSON时,它会查看该类型是否满足json.Marshaler接口,该接口如下所示:

type Marshaler interface 
    MarshalJSON() ([]byte, error)

如果类型确实满足接口,那么Go将调用它的MarshalJSON()方法,并使用它返回的[]byte切片作为JSON编码的值。

如果该类型没有MarshalJSON()方法,那么Go将返回尝试根据自己的内部规则将其编码为JSON。

因此,如果我们想定制某些类型的编码方式,只需要在其上实现MarshalJSON()方法,该方法以[]byte类型返回自定义的JSON内容。

提示:如果您查看time.Time类型源代码,就可以看到这一点。time.Time实际上是一个结构体,但是它有一个MarshalJSON()方法,输出RFC3339格式JSON对象。当time.Time值被序列化为JSON对象时,就会调用MarshalJSON()方法。

1.定制Runtime字段JSON序列化

为了说明这一点,让我们看一下应用程序中的一个具体示例。

当我们的Movie结构被编码为JSON时,Runtime字段(它是一个int32类型)编码为JSON数字。现在我们来更改它,将其编码为"<runtime> mins“的字符串。像这样:


    "id": 123,
    "title": "Casablanca",
    "runtime": "102 mins",
    "genres":
    [
        "drama",
        "romance",
        "war"
    ],
    "version": 1

有几种方法可以实现这一点,但一种简单的方法是为Runtime字段创建一个自定义类型,并在这个类型上实现MarshalJSON()方法。

为了防止internal/data/movie.go文件不会太乱,我们创建一个新的文件来处理runtime类型序列化逻辑:

 $ touch internal/data/runtime.go

然后继续添加以下代码:

package data

import (
    "fmt"
    "strconv"
)

//申明Runtime类型,其底层是int32类型(和movie中的字段一样)
type Runtime int32

//实现MarshalJSON()方法,这样就实现了json.Marshaler接口。
func (r Runtime) MarshalJSON() ([]byte, error) 
    //生成一个字符串包含电影时长
    jsonValue := fmt.Sprintf("%d mins", r)

    //使用strconv.Quote()函数封装双引号。为了在JSON中以字符串对象输出,需要用双引号。
    quotedJSONValue := strconv.Quote(jsonValue)
    //将字符串转为[]byte返回
    return []byte(quotedJSONValue),nil

这里我想强调两点:

  • 如果您的MarshalJSON()方法像我们的方法一样返回一个JSON字符串值,那么您必须在返回字符串之前用双引号包装它。否则它将不会被解释为JSON字符串,你将收到类似于这样的运行时错误:
    json: error calling MarshalJSON for type data.Runtime: invalid character 'm' after top-level value
    
  • 我们故意为MarshalJSON()方法使用值接收器,而不是指针接收器func (r *Runtime) MarshalJSON()。这给了我们更多的灵活性,因为这意味着定制JSON编码将对Runtime值对象和指针对象都有效。正如Effective Go提到的:

如果你不确定指针和值接收器之间的区别,那么这篇博客提供了一个很好的总结。

好的,现在有了自定义Runtime类型,打开internal/data/movies.go文件并更新Movie结构:

File: internal/data/movies.go

package data

import (
    "time"
)

type Movie struct 
    ID       int64     `json:"id"`
    CreateAt time.Time `json:"-"`
    Title    string    `json:"title"`
    Year     int32     `json:"year,omitempty"`
        //使用Runtime类型取代int32,注意omitempty还是能生效的
    Runtime  Runtime   `json:"runtime,omitempty,string"`
    Genres   []string  `json:"genres,omitempty"`
    Version  int32     `json:"version"`

重启服务然后对GET /v1/movies/:id接口发起请求。你应该看到一个包含自定义runtime值的响应,格式为"xx mins",类似如下:

$ curl localhost:4000/v1/movies/123

    "movie":
    
        "id": 123,
        "title": "Casablanca",
        "runtime": "102 mins",
        "genres":
        [
            "drama",
            "romance",
            "war"
        ],
        "version": 1
    

总之,这是定制JSON序列化的一种很好的方法。我们的代码简洁明了,并且我们有一个自定义的Runtime类型,可以随时随地使用它。

但也有不利的一面。在将代码与其他包集成时,使用自定义类型有时会很尴尬,您可能需要执行类型转换,将自定义类型转换为其他包理解和可接受的值。

以上是关于Go 定制JSON序列化的主要内容,如果未能解决你的问题,请参考以下文章

Day05 Go语言文件操作,结构体,构造函数,方法接收器,json序列化

Go websocket 序列化/反序列化 json

跟炒鸡辣鸡一起学用go写游戏后端2

将 JSON 反序列化为现有对象 (Java)

定制json序列化

json序列化时定制支持datetime类型,和到中文让他保留中文形式