【Go Web开发】解析JSON请求

解析JSON请求

到目前为止,我们一直在研究如何从我们的API中创建和发送JSON响应,在本文中,我们将从另一方面探索,讨论如何读取和解析来自客户端的JSON请求。

为了帮助说明这一点,我们将从POST /v1/movies接口和之前设置的createMovieHandler上开始工作。

Method URL Handler 动作
GET /v1/healthcheck healthcheckHanlder 查询应用程程序信息
POST /v1/movies createMovieHandler 创建新的电影
GET /v1/movies/:id showMovieHandler 查询特定电影详情

当客户端调用这个接口时,我们希望它们提供一个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 boolean bool
JSON string string
JSON number int, uint, float*, rune
JSON array array, slice
JSON object struct, map

使用json.Unmarshal函数

正如我们在本节开始时提到的,也可以使用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)。


你可能感兴趣的:(【Go Web开发】解析JSON请求)