美文网首页
【Go Web开发】解析JSON请求

【Go Web开发】解析JSON请求

作者: Go语言由浅入深 | 来源:发表于2022-01-21 23:59 被阅读0次

    解析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请求

          本文链接:https://www.haomeiwen.com/subject/ingyhrtx.html