到目前为止,我们一直在使用Firefox向API服务发送请求,由于内置的JSON查看器提供了“输出格式化”,这使得JSON响应易于阅读。
但是如果您尝试使用curl发起请求,将看到实际的JSON响应数据都在一行中,没有空格。
$ curl localhost:4000/v1/healthcheck
{"environment":"development","status":"available","version":"1.0.0"}
$ curl localhost:4000/v1/movies/123
{"id":123,"title":"Casablanca","runtime":102,"genres":["drama","romance","war"],"version":1}
通过使用json.MarshalIndent()函数来编码响应数据,而不是使用常规的json.Marshal()函数,可以使这些内容更容易在终端中查看。自动将空格符添加到JSON输出中,每个元素放在单独的行,并在每个行前面加上可选的前缀和缩进字符。
我们更新writeJSON()助手来使用下面的代码:
File: cmd/api/helpers.go
package main
...
func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, headers http.header) error {
//使用json.MarshalIndent()函数,可以在JSON输出中添加空格。这里使用""作为前缀,"\t"格式化元素
js, err := json.MarshalIndent(data, "", "\t")
if err != nil {
return err
}
js = append(js, '\n')
for key, value := range headers {
w.Header()[key] = value
}
w.Header.Set("ContentType", "application/json")
w.WriteHeader(status)
w.Write(js)
return nil
}
如果你重新启动API服务器并尝试从你的终端再次发出相同的请求,将会收到一些类似于以下内容,包含格式化空格的JSON响应:
$ curl -i localhost:4000/v1/healthcheck
{
"environment": "development",
"status": "available",
"version": "1.0.0",
}
$ curl localhost:4000/v1/movies/123
{
"id": 123,
"title": "casablanca",
"runtime": 102,
"genres": [
"drama",
"romance",
"war"
],
"version": 1
}
性能比价
虽然从可读性和用户体验的角度来看,使用json.MarshalIndent()是更适合,但不幸的是,它不是免费的。除了响应的总字节数稍微大一点之外,Go为添加空格所做的额外工作对性能有显著的影响。
下面的基准测试使用gist中的代码演示json.Marshal()和json.MarshalIndent()的相对性能。
在这些基准测试中,我们可以看到json.MarshalIndent()的运行时间比json.Marshal()长65%,使用的内存比json.Marshal()多30%,还多两次堆分配。这些数据会根据编码内容而变化,但根据我的经验,它们反映了性能影响。
但在大多数应用程序中这点性能不会造成担心。实际上,我们谈论的只是千分之一毫秒的时间——为了提高响应的可读性是值得这样做的。但是,如果你的API在资源非常受限的环境中运行,或者需要管理大级别的流量,那么这是需要注意的,您可能更倾向于使用json.Marshal()。
注意:JSON.marshalindent()的工作原理是像调用JSON.marshal()一样,然后通过独立的JSON.indent()函数为JSON添加空格。还有一个反向函数JSON.compact(),您可以使用它从JSON中删除空格。
封装响应
接下来,我们更新响应,使JSON数据始终包含在父JSON对象中。类似于:
{
"movie":
{
"id": 123,
"title": "Casablanca",
"runtime": 102,
"genres":
[
"drama",
"romance",
"war"
],
"version": 1
}
}
注意到电影对象是如何嵌套在键“movie”下面的,而不是本身作为顶层JSON对象。
严格来说,这样封装响应数据是没有必要的,是否选择这样做在一定程度上取决于你的编码风格和品味。但也有一些切实的好处:
1、在JSON的顶层包含一个键名(如“movie”)有助于使响应更具文档化。对于任何从上下文中看到响应的人来说,理解响应数据与什么相关更容易一些。
2、它降低了客户端出错的风险,因为这样做会避免把一个响应当作其他内容来处理。为了获得数据,客户端必须通过“movie”键显式地引用它。
3、如果我们总是封装API返回的数据,那么我们就减轻了在旧浏览器中返回JSON数组作为响应可能出现的安全漏洞。
我们可以使用一些技术来封装API响应,为了使事情简单,通过创建一个自定义的envelope map与底层类型map[string]interface{}来实现。
接下来将说明怎么封装响应:
从更新cmd/api/helper.go开始,如下所示:
File: cmd/api/helpers.go
package main
...
//定义envelop类型
type envelope map[string]interface{}
//将data参数类型改为envelope
func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.header) error {
js, err := json.MarshalIndent(data, "", "\t")
if err != nil {
return err
}
js = append(js, '\n')
for key, value := range headers {
w.Header()[key] = value
}
w.Header.Set("ContentType", "application/json")
w.WriteHeader(status)
w.write(js)
return nil
}
然后,我们需要更新showMovieHandler,以创建包含电影数据的envelope实例,并将其传递给writeJSON()函数,而不是直接传递电影结构体实例,如下所示:
File: cmd/api/movies.go
package main
...
func (app *application) showMovieHandler(w http.ResponseWriter, r http.Request) {
id, err := app.readIDParam(r)
if err != nil {
http.NotFound(w, r)
return
}
movie := data.Movie{
ID: id,
CreateAt: time.Now(),
Title: "Casablanca",
Runtime: 102,
Genres: []string{"drama", "romance", "war"},
Version: 1,
}
//创建envelope{"movie": movie}实例,然后传给writeJSON函数,而不是直接传movie结构体实例
err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
if err != nil {
app.logger.Println(err)
http.Error(w, "the server encountered a problem and could not process your request", http.StatusInternalServerError)
}
}
还需要更新healthcheckHandler中的代码,以便它也将envelope类型传递给writeJSON()函数:
File: cmd/api/healthcheck.go
package main
...
func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
//申明envelope类型包含响应数据。注意,我们构造它的方式意味着环境和版本数据现在将嵌套在JSON响应的system_info键下。
env := envelope{
"status": "available",
"system_info": map[string]string{
"environment": app.config.env,
"version": version,
},
}
err := app.writeJSON(w, http.StatusOK, env, nil)
if err != nil {
app.logger.Println(err)
http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError)
}
}
好了,我们来试试这些修改。重新启动服务器,然后使用curl向API接口发出请求。您现在应该得到如下格式的响应。
$ curl localhost:4000/v1/movies/123
{
"movie":
{
"id": 123,
"title": "Casablanca",
"runtime": 102,
"genres":
[
"drama",
"romance",
"war"
],
"version": 1
}
}
$ curl localhost:4000/v1/healthcheck
{
"status": "available",
"system_info":
{
"environment": "development",
"version": "1.0.0"
}
}
附加内容
HTTP响应结构
要强调下构造JSON响应没有唯一正确或错误方法。有一些流行的格式,如JSON:API和jsend,您可能希望遵循或使用它们来获得灵感,但这当然不是必需的,大多数API不遵循这些格式。
无论你做什么,考虑格式化和在不同的API接口之间保持清晰和一致的响应结构都是很有必要的——特别是当它们需要对外开放。