【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)。


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容