解析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)宪摧。