相关文章推荐
文雅的沙滩裤  ·  Navigator:canShare() ...·  2 月前    · 
发怒的花卷  ·  ASP.NET MVC 4 发行说明 | ...·  2 月前    · 
老实的橙子  ·  HTML、CSS 和 DOM ...·  2 月前    · 
神勇威武的葫芦  ·  DISM App Package ...·  1 年前    · 
礼貌的太阳  ·  WPF ...·  2 年前    · 

当客户端调用这个接口时,我们希望它们提供一个JSON请求体,其中包含想要在我们的系统中创建的电影的数据。例如,如果客户端想要为电影Moana添加一条记录到我们的API中,会发送一个类似于这样的请求体:

"title": "Moana", "year": 2016, "runtime": 107, "genres": "animation", "adventure"

现在,我们只关注处理这个JSON请求体的读取、解析和验证。接下来你将学习:

  • 如何使用encoding/json包读取请求体并将其反序列化为本地Go对象。
  • 如何处理来自客户端的错误请求和无效的JSON,并返回清晰的、可操作的错误消息。
  • 如何创建可重用的辅助程序来验证数据,以确保数据符合业务规则。
  • 控制和定制JSON解码方式的不同技术。
  • JSON解码(反序列化)

    和JSON编码一样,有两种方式可以用于将JSON解码为Go对象:使用json.Decoder类型和json.Unmarshal()函数。

    这两种方法各有优缺点,但为了从HTTP请求体解码JSON,使用JSON.Decoder通常是最好的选择。它比json.Unmarshal()更高效,需要更少的代码,并提供了一些有用的设置,您可以使用这些设置来调整其行为。

    用代码说明json.Decoder是如何工作会更简单,所以让我们直接进入代码,更新createMovieHandler处理函数:

    File: cmd/api/movies.go

    package main
    import (
        "encoding/json"
        "fmt"
        "net/http"
        "time"
        "greenlight.alexedwards.net/internal/data"
    func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
        //申明一个匿名结构体来接收HTTP请求体中对JSON内容,注意结构体中字段和类型与之前创建movie结构体只包含部分字段
        //该结构体定义类型用于接收http请求,并解码为Go对象。
        var input struct {
            Title     string   `json:"title"`
            Year      int32    `json:"year"`
            Runtime   int32    `json:"runtime"`
            Genres    []string `json:"genres"`
        //初始化json.Decoder实例,从http请求body中读取请求内容,然后使用Decode()方法将内容解析为input结构体。
        //注意Decoder函数接收对是指针类型,如果解析错误就调用errorResponse()帮助函数返回400错误给客户端。
        err := json.NewDecoder(r.Body).Decode(&input)
        if err != nil {
            app.errorResponse(w, r, http.StatusBadRequest, err.Error())
            return
        //将解析后对input结构体写入HTTP响应,返回给客户端
        fmt.Fprintf(w, "%+v\n", input)
    

    关于这段代码,有一些重要的地方需要指出:

  • 当调用Decoder()必须传入一个非nil指针作为解析对象的存储位置。如果传入的不是指针,运行时将返回json.InvalidUnmarshalError错误。
  • 如果传入的是结构体,像上面代码中的那样,结构体字段必须首字母大写。和编码一样,它们需要被导出,这样才能使它们对encoding/json包是可见的。
  • 当将JSON对象解码为结构体时,JSON中的键/值对将基于结构体标签名映射到结构体字段。如果没有匹配的结构体标签,Go将试图将对应的JSON值编码到匹配对结构体字段中,不区分大小写的匹配)。任何不能成功映射到结构体字段的JSON键/值对都将被忽略。
  • 在r.Body被读取之后,没有必要关闭它。这将由Go的http.Server自动处理。
  • 好吧,我们来试试。

    启动应用程序,然后打开第二个终端窗口,向POST /v1/ movies发出请求,其中包含一些movie数据。你应该会看到类似这样的响应:

    #创建一BODY变量,包含要发送对JSON内容
    $ BODY='{"title":"Moana","year":2016,"runtime":107, "genres":["animation","adventure"]}'
    #使用-d命令行参数将BODY内容发送给服务端
    $ curl -i -d "$BODY" localhost:4000/v1/movies
    HTTP/1.1 200 OK
    Date: Tue, 06 Apr 2021 17:13:46 GMT Content-Length: 65
    Content-Type: text/plain; charset=utf-8
    {Title:Moana Year:2016 Runtime:107 Genres:[animation adventure]}
    

    太棒了!似乎很有效。从响应数据可以看出,我们在请求体中提供的值已经被解码到input结构体的对应字段中。

    让我们快速看一下如果我们在JSON请求体中忽略特定的键/值对会发生什么。例如,在JSON中创建一个没有year字段的请求,如下所示:

    $ BODY='{"title":"Moana","runtime":107, "genres":["animation","adventure"]}' 
    $ curl -d "$BODY" localhost:4000/v1/movies
    {Title:Moana Year:0 Runtime:107 Genres:[animation
    

    正如您可能已经猜到的,当我们这样做时,输入结构中的Year字段将保留其零值(碰巧是0,因为Year字段是一个int32类型)。

    这就引出了一个有趣的问题:如何区分客户端不提供键/值对和提供键/值对但故意将其设置为零的情况?例如:

    $ BODY='{"title":"Moana","year":0,"runtime":107, "genres":["animation","adventure"]}' 
    $ curl -d "$BODY" localhost:4000/v1/movies
    {Title:Moana Year:0 Runtime:107 Genres:[animation adventure]}
    

    尽管HTTP请求不同,但最终结果是相同的,并且如何区分这两种场景并不是很明显。我们后面再回到这个话题,但现在,只需要了解这个特殊情况就够了。

    解码支持的目标类型

    值得一提的是,某些JSON类型只能成功解码为某些Go类型。例如,如果你有JSON字符串“foo”,它可以被解码成一个Go字符串,但试图将其解码成一个Go int或bool将导致运行时错误(我们将在下一节中演示)。

    下表提供了不同JSON类型支持解码为对应的GO类型:

    JSON 类型 支持的Go类型

    正如我们在本节开始时提到的,也可以使用json.Unmarshal()函数来解码HTTP请求体。

    例如,你可以像这样在处理程序中使用它:

    func (app *application) exampleHandler(w http.ResponseWriter, r *http.Request) {
        var input struct {
            Foo string `json:"foo"`
        //使用io.ReadAll()读取整个HTTP请求body内容到[]byte切片中
        body, err := io.ReadAll(r.Body)
        if err != nil {
            app.serverErrorResponse(w, r, err)
            return
        //使用json.Unmarshal()函数将切片中的JSON解码到input结构体。再次说明使用的参数是指针。
        err = json.Unmarshal(body, &input)
        if err != nil {
            app.errorResponse(w, r, http.StatusBadRequest, err.Error())
        fmt.Fprintf(w, "%+v\n", input)
    

    使用这种方法很简单。但没有我们之前提到的json.Decoder方法中的优点。

    不仅代码稍微更冗长,而且效率也更低。如果我们对这个特定用例的相对性能进行基准测试,可以看到使用json. unmarshal()比json.Decoder多损耗80%的内存(B/op)。以及稍微慢一点(ns/op)。