JSON编码
我们继续看一些更令人兴奋的内容,看看如何将Go对象(如map、结构体和切片)编码为JSON。
从宏观角度看,Go的encoding/json包提供了两个选择来将内容编码为json。你可以调用json.marshal()函数,或者你可以声明并使用json.Encoder类型。
我们将在本章中解释这两种方法是如何工作的,但是为了在HTTP响应中发送JSON,使用JSON.marshal()通常是更好的选择。所以我们从这里开始。
JSON.marshal()的工作方式在概念上非常简单——您将一个Go对象作为参数传递给它,它将以字节数组形式返回该对象的JSON表示。函数签名看起来像这样:
func Marshal(v interface{}) ([]byte, error)
注意:上述方法中的v参数的类型为interface{}(称为空接口)。这实际上意味着我们能够将任何Go类型作为v参数传递给Marshal()。
我们开始并更新healthcheckHandler方法,以便它使用JSON.marshal()直接从Go map生成JSON响应——而不是像之前那样使用固定格式的字符串。像这样:
File: cmd/api/healthcheck.go
package main
import(
"encoding/json"
"net/http"
)
func (app *application)healthcheckHandler(w http.ResponseWriter, r *http.Request) {
//创建一个map包含我们想发送的响应内容
data := map[string]string{
"status": "available",
"environment": app.config.env,
"version": version,
}
//将map对象传给json.Marshal()函数。序列号成byte数组包含编码后的JSON内容。如果有错误就打印到日志
//向客户端发送一个错误消息
js, err := json.Marshal(data)
if err != nil {
app.logger.Println(err)
http.Error(w, "the server encountered a problem and could not process request", http.StatusInternalServerError)
return
}
//向JSON中添加换行符。这主要是有助于在终端上看起来方便
js = append(js, '\n')
//此时编码已经完成了,因此需要为HTTP添加响应header,通知客户端接收JSON格式内容
w.Header().Set("ContentType", "application/json")
//使用w.Write()发送包含JSON内容的字节数组
w.Write(js)
}
如果你重新启动API并在浏览器中访问localhost:4000/v1/healthcheck,你现在应该会得到类似这样的响应:
这看起来很不错——我们可以看到map对象已经自动编码为JSON对象,map中的键/值对在JSON对象中按字母顺序排序的。
创建一个writeJSON帮助方法
随着API的增长,我们将发送大量JSON响应,因此有必要将其中一些逻辑转移到可重用的writeJSON()帮助方法中。
除了创建和发送JSON外,我们还希望通过这个帮助函数,在以后的正常响应中可以包含任意响应头信息,比如在系统中创建新电影后的Location头信息。
如果你跟随文章编码的话,打开cmd/api/helpers.go文件并创建以下writeJSON()方法:
File: cmd/api/helper.go
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
//定义writeJSON()帮组函数来发送响应。方法以http.ResponseWrite作为响应写入地方,
//HTTP状态码status,需要编码为JSON的数据data和一个响应头map对象
func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, headers http.Header) error {
//将data编码为JSON,如果有错误就返回
js, err := json.Marshal(data)
if err != nil {
return err
}
//附加一个换行符以使它更容易在终端应用程序中查看。
js = append(js, '\n')
//此时,我们知道在发送响应之前不会再遇到任何错误,所以添加任何我们想要包含的响应头都是安全的。
//循环迭代headers(map类型)将对应的header添加到http.ResponseWriter响应头。注意map是nil也不会报错
for key, value := range headers {
w.Header().Set(k) = value
}
//添加"ContentType"响应头,然后写入状态码和JSON内容
w.Header().Set("ContentType", "application/json")
w.WriteHeader(status)
w.Write(js)
return nil
}
现在writeJSON()帮助函数已经就位,我们可以显著地简化healthcheckHandler中的代码:
File: cmd/api/healthcheck.go
package main
import (
"net/http"
)
func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]string{
"status": "available",
"environment": app.config.env,
"version": version,
}
err := app.writeJSON(w, http.StatusOK, data, nil)
if err != nil {
app.logger.Println(err)
http.Error(w, "The server encountered a problem and could not process you request", http.StatusInternalServerError)
}
}
如果现在再次运行应用程序,一切都编译正确,对GET /v1/healthcheck接口的请求应该会得到与之前相同的HTTP响应。
附加内容
不同的Go类型是如何编码的
在本章中,我们已经将一个map[string]string类型编码为JSON,其键/值对中的值为JSON字符串。但是Go也支持编码其他基本类型。
下表总结了不同的Go类型在编码过程中是如何映射到JSON数据类型的:
Go 类型 | jSON 类型 |
---|---|
bool | JSON boolean |
string | JSON string |
int, uint, float*, rune | JSON number |
array, slice | JSON array |
struct, map | JSON object |
nil pointers, interface values, slices, maps, etc | JSON null |
chan, func, complex* | 不支持 |
time.Time | RRC3339格式字符串 |
[]byte | Base64编码的JSON字符串 |
最后两个是特殊情况,需要更多的解释:
- time.Time值(这实际上是一个结构体)将根据RFC3339格式来编码成JSON字符串类似“2020-11-08T06:27:59+01:00”,而不是JSON对象。
- []byte字节数组将编码为base64类型的JSON字符串,而不是一个JSON数组。因此[]byte{'h','e','l','l','o'}将编码为"aGVsbG8="。base64编码使用填充和标准字符集。
其他需要指出的是:
- 支持嵌套对象的编码。例如,如果你有一个结构体切片,Go将编码成JSON对象数组。
- 不能对channel、函数和复数类型进行编码。如果你想这么做, 你会得到一个json.UnsupportedTypeError。
- 任何指针值都将被编码为所指向的值。同样,interface{}值也会编码为接口中包含的值。
使用json.Encoder
在本章的开头,我提到过也可以使用Go的json.Encoder类型来编码。它支持将对象编码为JSON对象并将JSON写入到一个输出流中,而且两个操作一步到位。
例如,你可以在处理程序中这样使用:
func (app *application) exampleHandler(w http.ResponseWrite, r *http.Request){
data := map[string]string{
"hello": "world",
}
//设置“ContentType”响应头
w.Header().Set("ContentType", "application/json")
//使用json.NewEncoder()函数初始化json.Encoder实例,将内容接入http.ResponseWriter。
//然后调用Encode()方法,将希望编码为JSON的data传入,如果data可以成功编码为JSON,它将
//编码后写入到http.ResponseWriter。
err := json.NewEncoder(w).Encode(data)
if err != nil {
app.logger.Println(err)
http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError)
}
}
这种方式是可行的,非常整洁和优雅,但如果你仔细考虑它,你可能会注意到一个小问题……
当我们调用JSON.NewEncoder(w).Encode(data)时,在一行代码中JSON被创建并写入http.ResponseWriter中,这意味着没有机会根据Encode()方法是否返回错误来设置HTTP响应头。
假设您想在一个成功的响应上设置一个Cache-Control报头,但如果JSON编码失败,则不设置Cache-Control报头而必须返回一个错误响应。使用json.Encoder模式就比较难实现。
你可以设置Cache-Control响应头,如果出现错误可以从响应头删除,但这都是很老套的。
另一个选择是将JSON写入bytes.Buffer缓存,而不是直接写入http.ResponseWriter。在设置Cache-Control响应头之前,检查任何错误。然后将JSON内容从bytes.Buffer缓存拷贝到http.ResponseWriter。但你真正那么处理的话,相比较而言使用json.Marshal()方法更简单。
json.Encoder和json.Marshal性能
谈到速度,您可能想知道json.Encoder和json.Marshal()之间是否有差异。简单来讲是肯定的……但是差别很小,在大多数情况下你不需要担心。
下面的基准测试使用本gist中的代码演示了这两种方法的性能(注意每个基准测试重复三次):
在这些结果中,我们可以看到json.marshal()要比json.Encoder稍微多一点的内存(B/op),并使用更多的堆内存分配(allocs/op)。
这两种方法在平均运行时(ns/op)上没有明显可观察到的差异。也许在更大的基准测试样本或更大的数据集,差异可能会变得明显,但它可能也是微秒级的,而不是更大。