在本系列文章前面,我們在API中添加了一些自定義的JSON編碼處理借跪,以便在JSON響應(yīng)中以“(runtime) mins”的格式顯示電影時長信息政己。
在本章,我們將從另一方面考慮掏愁,如何將“(runtime) mins”字段解析到int32類型的Go結(jié)構(gòu)體字段中歇由,也稱為反序列化處理。
如果你現(xiàn)在嘗試用"(runtime) mins"這種格式發(fā)送一個請求果港,會得到一個400 Bad request響應(yīng)(因?yàn)椴豢赡軐SON字符串("107 mins")解碼為int32類型)沦泌。如下所示:
$ curl -d '{"title": "Moana", "runtime": "107 mins"}' localhost:4000/v1/movies
{
"error": "body contains incorrect JSON type for \"runtime\""
}
為了使其正常工作,我們需要攔截JSON解碼過程辛掠,并手動將“<runtime> mins”JSON字符串轉(zhuǎn)換為int32谢谦。
因此我們該如何處理呢?
json.Unmarshaler接口
這里的關(guān)鍵是了解Go的json.Unmarshaler接口萝衩,它看起來像這樣:
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
當(dāng)Go解碼JSON時回挽,會檢查目標(biāo)字段的類型是否實(shí)現(xiàn)了json.Unmarshaler接口。如果實(shí)現(xiàn)了該接口猩谊,Go將調(diào)用UnmarshalJSON()函數(shù)來決定如何將JSON解碼到目標(biāo)類型千劈。這剛好與json.Marshaler接口相反,我們之前使用它來定制JSON編碼行為预柒。
我們看看如何實(shí)現(xiàn)這個接口队塘。
首先我們需要做的第一件事是更新createMovieHandler,以便輸入結(jié)構(gòu)體使用我們自定義的Runtime類型宜鸯,而不是常規(guī)的int32。你應(yīng)該記得遮怜,我們的runtime類型仍然是基礎(chǔ)類型int32淋袖,但通過自定義類型,我們可以自由地讓它上實(shí)現(xiàn)UnmarshalJSON()方法锯梁。
下面更新接口處理程序如下:
File:cmd/api/movies.go
package main
...
func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
var input struct {
Title string `json:"title"`
Year int32 `json:"year"`
Runtime data.Runtime `json:"runtime"`
Genres []string `json:"genres"`
}
err := app.readJSON(w, r, &input)
if err != nil {
app.badRequestResponse(w, r, err)
return
}
fmt.Fprintf(w, "%+v\n", input)
}
然后我們轉(zhuǎn)到internal/data/runtime.go文件即碗,為Runtime類型添加UnmarshalJSON方法焰情。在這個方法里面需要解析“(runtime) mins”格式的JSON字符串,然后將runtime數(shù)值解析為int32剥懒,并將其賦值給Runtime本身内舟。
這實(shí)際上有點(diǎn)復(fù)雜,有一些重要的細(xì)節(jié)初橘,最好直接進(jìn)入代碼验游,并在后面用注釋進(jìn)行解釋。
File:internal/data/runtime.go
package data
import (
"errors"
"fmt"
"strconv"
"strings"
)
//如果UnmarshalJSON函數(shù)不能將JSON字符串解析為Runtime類型保檐,就返回該錯誤
var ErrInvalidRuntimeFormat = errors.New("invalid runtime format")
type Runtime int32
//Runtime類型實(shí)現(xiàn)UnmarshalJSON()方法耕蝉,這樣就實(shí)現(xiàn)了json.Unmarshaler接口。注意夜只,因?yàn)閁nmarshalJSON()會改變狀態(tài)
//因此需要使用指針接收者垒在。否則就要返回一個實(shí)例的拷貝。
func (r *Runtime) UnmarshalJSON(jsonValue []byte) error {
//希望接收到的值是一個JSON字符串"(runtime) mins"扔亥,第一步需要去掉雙引號场躯。
unquotedJSONValue, err := strconv.Unquote(string(jsonValue))
if err != nil {
return ErrInvalidRuntimeFormat
}
//分割字符串以分離包含數(shù)字的部分
parts := strings.Split(unquotedJSONValue, " ")
if len(parts) != 2 || parts[1] != "mins" {
return ErrInvalidRuntimeFormat
}
//將數(shù)字字符串轉(zhuǎn)為int32類型
i, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return ErrInvalidRuntimeFormat
}
//將int32強(qiáng)制轉(zhuǎn)為Runtime類型,并賦值給接收者旅挤,注意是指針類型的接收者踢关。
*r = Runtime(i)
return nil
}
完成以上代碼后重啟服務(wù),然后使用"(runtime) min"格式的值作為JSON字段谦铃≡懦桑可以看到請求會被正常處理。數(shù)值會從字符串中提取出來賦值給input結(jié)構(gòu)體中的Runtime類型字段驹闰,如下所示:
$ curl -d '{"title": "Moana", "runtime": "107 mins"}' localhost:4000/v1/movies
{Title:Moana Year:0 Runtime:107 Genres:[]}
然而瘪菌,如果你使用JSON數(shù)字或任何其他格式發(fā)請求,你應(yīng)該會得到一個ErrInvalidRuntimeFormat錯誤消息的響應(yīng)嘹朗,類似如下:
$ curl -d '{"title": "Moana", "runtime": 107}' localhost:4000/v1/movies
{
"error": "invalid runtime format"
}
$ curl -d '{"title": "Moana", "runtime": "107 minutes"}' localhost:4000/v1/movies
{
"error": "invalid runtime format"
}