程序员社区

【Go Web开发】JSON编码

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,你现在应该会得到类似这样的响应:

【Go Web开发】JSON编码插图
固定JSON.png

这看起来很不错——我们可以看到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中的代码演示了这两种方法的性能(注意每个基准测试重复三次):

【Go Web开发】JSON编码插图1

在这些结果中,我们可以看到json.marshal()要比json.Encoder稍微多一点的内存(B/op),并使用更多的堆内存分配(allocs/op)。

这两种方法在平均运行时(ns/op)上没有明显可观察到的差异。也许在更大的基准测试样本或更大的数据集,差异可能会变得明显,但它可能也是微秒级的,而不是更大。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 【Go Web开发】JSON编码

一个分享Java & Python知识的社区