【Go Web開發(fā)】解析JSON請求

解析JSON請求

到目前為止赛惩,我們一直在研究如何從我們的API中創(chuàng)建和發(fā)送JSON響應煌茬,在本文中址否,我們將從另一方面探索局蚀,討論如何讀取和解析來自客戶端的JSON請求麦锯。

為了幫助說明這一點,我們將從POST /v1/movies接口和之前設置的createMovieHandler上開始工作琅绅。

Method URL Handler 動作
GET /v1/healthcheck healthcheckHanlder 查詢應用程程序信息
POST /v1/movies createMovieHandler 創(chuàng)建新的電影
GET /v1/movies/:id showMovieHandler 查詢特定電影詳情

當客戶端調用這個接口時扶欣,我們希望它們提供一個JSON請求體,其中包含想要在我們的系統(tǒng)中創(chuàng)建的電影的數(shù)據(jù)千扶。例如料祠,如果客戶端想要為電影Moana添加一條記錄到我們的API中,會發(fā)送一個類似于這樣的請求體:

{
    "title": "Moana",
    "year": 2016,
    "runtime": 107,
    "genres":
    [
        "animation",
        "adventure"
    ]
}

現(xiàn)在澎羞,我們只關注處理這個JSON請求體的讀取术陶、解析和驗證。接下來你將學習:

  • 如何使用encoding/json包讀取請求體并將其反序列化為本地Go對象煤痕。
  • 如何處理來自客戶端的錯誤請求和無效的JSON,并返回清晰的接谨、可操作的錯誤消息摆碉。
  • 如何創(chuàng)建可重用的輔助程序來驗證數(shù)據(jù),以確保數(shù)據(jù)符合業(yè)務規(guī)則脓豪。
  • 控制和定制JSON解碼方式的不同技術巷帝。

JSON解碼(反序列化)

和JSON編碼一樣,有兩種方式可以用于將JSON解碼為Go對象:使用json.Decoder類型和json.Unmarshal()函數(shù)扫夜。

這兩種方法各有優(yōu)缺點楞泼,但為了從HTTP請求體解碼JSON,使用JSON.Decoder通常是最好的選擇笤闯。它比json.Unmarshal()更高效堕阔,需要更少的代碼,并提供了一些有用的設置颗味,您可以使用這些設置來調整其行為超陆。

用代碼說明json.Decoder是如何工作會更簡單,所以讓我們直接進入代碼浦马,更新createMovieHandler處理函數(shù):

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內(nèi)容时呀,注意結構體中字段和類型與之前創(chuàng)建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中讀取請求內(nèi)容,然后使用Decode()方法將內(nèi)容解析為input結構體磺陡。
    //注意Decoder函數(shù)接收對是指針類型趴梢,如果解析錯誤就調用errorResponse()幫助函數(shù)返回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值編碼到匹配對結構體字段中焦履,不區(qū)分大小寫的匹配)。任何不能成功映射到結構體字段的JSON鍵/值對都將被忽略雏逾。
  • 在r.Body被讀取之后嘉裤,沒有必要關閉它。這將由Go的http.Server自動處理栖博。

好吧屑宠,我們來試試。

啟動應用程序仇让,然后打開第二個終端窗口典奉,向POST /v1/ movies發(fā)出請求,其中包含一些movie數(shù)據(jù)丧叽。你應該會看到類似這樣的響應:

#創(chuàng)建一BODY變量卫玖,包含要發(fā)送對JSON內(nèi)容
$ BODY='{"title":"Moana","year":2016,"runtime":107, "genres":["animation","adventure"]}'

#使用-d命令行參數(shù)將BODY內(nèi)容發(fā)送給服務端
$ 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]}

太棒了!似乎很有效。從響應數(shù)據(jù)可以看出踊淳,我們在請求體中提供的值已經(jīng)被解碼到input結構體的對應字段中骇笔。

零值

讓我們快速看一下如果我們在JSON請求體中忽略特定的鍵/值對會發(fā)生什么。例如嚣崭,在JSON中創(chuàng)建一個沒有year字段的請求笨触,如下所示:

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

正如您可能已經(jīng)猜到的,當我們這樣做時雹舀,輸入結構中的Year字段將保留其零值(碰巧是0芦劣,因為Year字段是一個int32類型)。

這就引出了一個有趣的問題:如何區(qū)分客戶端不提供鍵/值對和提供鍵/值對但故意將其設置為零的情況说榆?例如:

$ 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請求不同虚吟,但最終結果是相同的寸认,并且如何區(qū)分這兩種場景并不是很明顯。我們后面再回到這個話題串慰,但現(xiàn)在偏塞,只需要了解這個特殊情況就夠了。

附加內(nèi)容

解碼支持的目標類型

值得一提的是邦鲫,某些JSON類型只能成功解碼為某些Go類型灸叼。例如,如果你有JSON字符串“foo”庆捺,它可以被解碼成一個Go字符串古今,但試圖將其解碼成一個Go int或bool將導致運行時錯誤(我們將在下一節(jié)中演示)。

下表提供了不同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函數(shù)

正如我們在本節(jié)開始時提到的滔以,也可以使用json.Unmarshal()函數(shù)來解碼HTTP請求體捉腥。

例如,你可以像這樣在處理程序中使用它:

func (app *application) exampleHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Foo string `json:"foo"`
    }

    //使用io.ReadAll()讀取整個HTTP請求body內(nèi)容到[]byte切片中
    body, err := io.ReadAll(r.Body)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }
  
    //使用json.Unmarshal()函數(shù)將切片中的JSON解碼到input結構體你画。再次說明使用的參數(shù)是指針抵碟。
    err = json.Unmarshal(body, &input)
    if err != nil {
        app.errorResponse(w, r, http.StatusBadRequest, err.Error())
    }

    fmt.Fprintf(w, "%+v\n", input)

    ...
}

使用這種方法很簡單。但沒有我們之前提到的json.Decoder方法中的優(yōu)點坏匪。

不僅代碼稍微更冗長立磁,而且效率也更低。如果我們對這個特定用例的相對性能進行基準測試剥槐,可以看到使用json. unmarshal()比json.Decoder多損耗80%的內(nèi)存(B/op)。以及稍微慢一點(ns/op)宪摧。


?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末粒竖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子几于,更是在濱河造成了極大的恐慌蕊苗,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沿彭,死亡現(xiàn)場離奇詭異朽砰,居然都是意外死亡,警方通過查閱死者的電腦和手機喉刘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門瞧柔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人睦裳,你說我怎么就攤上這事造锅。” “怎么了廉邑?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵哥蔚,是天一觀的道長倒谷。 經(jīng)常有香客問我,道長糙箍,這世上最難降的妖魔是什么渤愁? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮深夯,結果婚禮上抖格,老公的妹妹穿的比我還像新娘。我一直安慰自己塌西,他們只是感情好他挎,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著捡需,像睡著了一般办桨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上站辉,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天呢撞,我揣著相機與錄音,去河邊找鬼饰剥。 笑死殊霞,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的汰蓉。 我是一名探鬼主播绷蹲,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼顾孽!你這毒婦竟也來了祝钢?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤若厚,失蹤者是張志新(化名)和其女友劉穎拦英,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體测秸,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡疤估,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了霎冯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铃拇。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沈撞,靈堂內(nèi)的尸體忽然破棺而出锚贱,到底是詐尸還是另有隱情,我是刑警寧澤关串,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布拧廊,位于F島的核電站监徘,受9級特大地震影響,放射性物質發(fā)生泄漏吧碾。R本人自食惡果不足惜凰盔,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望倦春。 院中可真熱鬧户敬,春花似錦、人聲如沸睁本。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呢堰。三九已至抄瑟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間枉疼,已是汗流浹背皮假。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留骂维,地道東北人惹资。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像航闺,于是被迫代替她去往敵國和親褪测。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容