高级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()方法。
定制电影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)
}
这里我想强调两点:
- 如果您的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类型,可以随时随地使用它。
但也有不利的一面。在将代码与其他包集成时,使用自定义类型有时会很尴尬,您可能需要执行类型转换,将自定义类型转换为其他包理解和可接受的值。