【Go Web開(kāi)發(fā)】校驗(yàn)JSON內(nèi)容

備注:【Go Web開(kāi)發(fā)】是一個(gè)從零開(kāi)始創(chuàng)建關(guān)于電影管理的Web項(xiàng)目。

在許多情況下谷誓,您需要對(duì)來(lái)自客戶端的數(shù)據(jù)執(zhí)行額外的驗(yàn)證或檢查绒障,以確保它在處理之前滿足特定的業(yè)務(wù)規(guī)則。在本文中捍歪,我們將通過(guò)更新createMovieHandler來(lái)演示如何在JSON API的上下文中做到這一點(diǎn):

  • 客戶端提供的movie標(biāo)題不為空户辱,長(zhǎng)度不超過(guò)500字節(jié)。
  • movie的year字段不能是空的糙臼,而且是在1888年到今年之間庐镐。
  • runtime字段不能空,而且是一個(gè)正數(shù)变逃。
  • genres字段包含1-5個(gè)不同的電影類型必逆。

如果其中任何一個(gè)檢查失敗,我們希望向客戶端發(fā)送一個(gè)422 Unprocessable Entity響應(yīng)揽乱,以及清楚地描述驗(yàn)證失敗的錯(cuò)誤消息名眉。

創(chuàng)建validator包

為了在整個(gè)項(xiàng)目中幫助我們進(jìn)行驗(yàn)證,將創(chuàng)建一個(gè)internal/validator包凰棉,其中包含一些簡(jiǎn)單的可重用的幫助類型和函數(shù)损拢。如果您正在跟隨本文操作,請(qǐng)?jiān)谀臋C(jī)器上創(chuàng)建以下目錄和文件:

$ mkdir internal/validator
$ touch internal/validator/validator.go

然后在文件internal/validator/validator.go中添加如下代碼:

package validator

import "regexp"

var (
        //申明一個(gè)正則表達(dá)式用于檢查email的格式撒犀,如果你有興趣該正則表達(dá)式來(lái)自于“https://html.spec.whatwg.org/#valid-e-mail-address”網(wǎng)站福压。
    EmailRx = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\. [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
)

//定義一個(gè)新的Validator類型,其中包含驗(yàn)證錯(cuò)誤的map或舞。
type Validator struct {
    Errors map[string]string
}

//New是一個(gè)構(gòu)造函數(shù)荆姆,用于創(chuàng)建Validator實(shí)例
func New() *Validator {
    return &Validator{Errors: make(map[string]string)}
}

//valid 返回true如果map中沒(méi)有錯(cuò)誤
func (v *Validator) Valid() bool {
    return len(v.Errors) == 0
}

//AddError 向map中添加錯(cuò)誤(map中不存在對(duì)應(yīng)key的錯(cuò)誤)
func (v *Validator) AddError(key, message string) {
    if _, exists := v.Errors[key]; !exists {
        v.Errors[key] = message
    }
}

//Check 向map中添加錯(cuò)誤消息,如果校驗(yàn)失敗即ok為false
func (v *Validator) Check(ok bool, key, message string) {
    if !ok {
        v.AddError(key, message)
    }
}

//In 如果list切片中存在value字符串返回true
func In(value string, list ...string) bool {
    for i := range list {
        if value == list[i] {
            return true
        }
    }
    return false
}

//Match 如果字符串滿足正則表達(dá)式就返回true
func Matches(value string, rx *regexp.Regexp) bool {
    return rx.MatchString(value)
}

//如果切片中的字符串都不同返回true
func Unique(values []string) bool {
    uniqueValues := make(map[string]bool)
    for _, value := range values {
        uniqueValues[value] = true
    }
    return len(values) == len(uniqueValues)
}

總結(jié):

在上面的代碼中定義了Validator類型映凳,包含一個(gè)存儲(chǔ)錯(cuò)誤信息map字段胆筒。Validator提供了Check()方法,根據(jù)校驗(yàn)結(jié)果向map中添加錯(cuò)誤信息魏宽,而Valid()方法返回map是否包含錯(cuò)誤信息腐泻。還添加了In(), Matches()和Unique()方法來(lái)幫助我們執(zhí)行特定字段的檢查。

從概念上講队询,這個(gè)Validator類型是非常簡(jiǎn)單的派桩,但這并不是一件壞事。正如我們將在其他地方看到的蚌斩,它在開(kāi)發(fā)中功能強(qiáng)大铆惑,為我們提供了很多靈活性的字段檢查。

執(zhí)行字段檢查

下面我們把validator類型使用起來(lái)。

我們需要做的第一件事是更新cmd/api/errors.go文件员魏,添加一個(gè)新的failedValidationResponse()幫助函數(shù)丑蛤,它將寫入一個(gè)422 Unprocessable Entity錯(cuò)誤碼,并將來(lái)自新Validator類型的錯(cuò)誤內(nèi)容映射為JSON響應(yīng)體撕阎。

File: cmd/api/errors.go


package main

...

//注意errors參數(shù)是一個(gè)map類型受裹,和validator類型包含map一致
func (app *application) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
    app.errorResponse(w, r, http.StatusUnprocessableEntity, errors)
}

完成之后,返回到createMovieHandler并更新它虏束,以對(duì)input結(jié)構(gòu)體各個(gè)字段進(jìn)行必要的檢查棉饶。像這樣:

File:cmd/api/movies.go


package main

import (
    "fmt"
    "net/http"
    "time"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/validator"
)

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
    }
    movie := &data.Movie{
        Title:   input.Title,
        Year:    input.Year,
        Runtime: input.Runtime,
        Genres:  input.Genres,
    }
        //初始化一個(gè)新的Validator實(shí)例
    v := validator.New()
  
    //使用Check()方法執(zhí)行字段校驗(yàn)。如果校驗(yàn)失敗就會(huì)向map中添加錯(cuò)誤信息镇匀。例如下面第一行檢查title不能為空照藻,然后再檢查長(zhǎng)度不能超過(guò)500字節(jié)等等。
    v.Check(movie.Title != "", "title", "must be provide")
    v.Check(len(movie.Title) <= 500, "title", "must not be more than 500 bytes long")

    v.Check(movie.Year != 0, "year", "must be provided")
    v.Check(movie.Year >= 1888, "year", "must be greater than 1888")
    v.Check(movie.Year <= int32(time.Now().Year()), "year", "must not be in the future")

    v.Check(movie.Runtime != 0, "runtime", "must be provided")
    v.Check(movie.Runtime > 0, "runtime", "must be a positive integer")

    v.Check(movie != nil, "genres", "must be provided")
    v.Check(len(movie.Genres) >= 1, "genres", "must contain at least 1 genres")
    v.Check(len(movie.Genres) <= 5, "genres", "must not contain more than 5 genres")
    //使用Unique()方法汗侵,檢查input.Genres每個(gè)字段是否唯一幸缕。
    v.Check(validator.Unique(movie.Genres), "genres", "must not contain duplicate values")

    //使用Valid()方法確認(rèn)檢查是否通過(guò)。如果有錯(cuò)誤就使用failedValidationResponse()幫助函數(shù)返回錯(cuò)誤信息給客戶端晰韵。
   if !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }
    fmt.Fprintf(w, "%+v\n", input)
}

做完這個(gè)之后发乔,我們就可以試一下了。重新啟動(dòng)服務(wù)宫屠,然后向post /v1/movie接口發(fā)送請(qǐng)求列疗,其中包含一些不合法的字段信息滑蚯,類似下面:

$ BODY='{"title":"","year":1000,"runtime":"-123 mins","genres":["sci-fi","sci-fi"]}' $ curl -i -d "$BODY" localhost:4000/v1/movies
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Date: Wed, 07 Apr 2021 10:33:57 GMT 
Content-Length: 180

{
    "error":
    {
        "genres": "must not contain duplicate values",
        "runtime": "must be a positive integer",
        "title": "must be provided",
        "year": "must be greater than 1888"
    }
}

看起來(lái)不錯(cuò)浪蹂。我們的檢查功能生效了,并阻止請(qǐng)求被執(zhí)行—甚至更好的是告材,向客戶端返回一個(gè)格式良好的JSON響應(yīng)坤次,其中包含針對(duì)每個(gè)檢驗(yàn)錯(cuò)誤的詳細(xì)信息。

你也可以發(fā)送正常的請(qǐng)求體斥赋,你會(huì)發(fā)現(xiàn)請(qǐng)求被正常處理缰猴,input內(nèi)容在響應(yīng)中返回給客戶端:

$ BODY='{"title":"Moana","year":2016,"runtime":"107 mins","genres":["animation","adventure"]}'
$ curl -i -d "$BODY" localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 23 Nov 2021 12:33:45 GMT
Content-Length: 65
Content-Type: text/plain; charset=utf-8

{Title:Moana Year:2016 Runtime:107 Genres:[animation adventure]}

使校驗(yàn)規(guī)則可重用

在大型項(xiàng)目中,很多個(gè)接口需要重復(fù)這種校驗(yàn)的過(guò)程疤剑,因此將上面的校驗(yàn)規(guī)則抽象成方法供其他地方使用滑绒。比如客戶端要更新movie也會(huì)傳一些新的字段內(nèi)容,也需要校驗(yàn)隘膘。

避免重復(fù)疑故,我們可以將movie的校驗(yàn)整合到一個(gè)單獨(dú)的ValidateMovie()函數(shù)中去。理論上弯菊,這個(gè)函數(shù)可以放在任意位置纵势。但就個(gè)人而言,我喜歡將驗(yàn)證檢查放在internal/data包中的相關(guān)領(lǐng)域類型附近。

如果按照下面的步驟操作钦铁,請(qǐng)重新打開(kāi)internal/data/movies.go然后添加一個(gè)ValidateMovie()函數(shù)软舌,其中包含如下檢查:

File: internal/data/movies.go


package data

import (
    "encoding/json"
    "fmt"
    "greenlight.alexedwards.net/internal/validator"
    "time"
)

type Movie struct {
    ID       int64     `json:"id"`
    CreateAt time.Time `json:"-"`
    Title    string    `json:"title"`
    Year     int32     `json:"year,omitempty"`
    Runtime  Runtime   `json:"runtime,omitempty,string"`
    Genres   []string  `json:"genres,omitempty"`
    Version  int32     `json:"version"`
}

func ValidateMovie(v *validator.Validator, movie *Movie) {
    v.Check(movie.Title != "", "title", "must be provide")
    v.Check(len(movie.Title) <= 500, "title", "must not be more than 500 bytes long")

    v.Check(movie.Year != 0, "year", "must be provided")
    v.Check(movie.Year >= 1888, "year", "must be greater than 1888")
    v.Check(movie.Year <= int32(time.Now().Year()), "year", "must not be in the future")

    v.Check(movie.Runtime != 0, "runtime", "must be provided")
    v.Check(movie.Runtime > 0, "runtime", "must be a positive integer")

    v.Check(movie != nil, "genres", "must be provided")
    v.Check(len(movie.Genres) >= 1, "genres", "must contain at least 1 genres")
    v.Check(len(movie.Genres) <= 5, "genres", "must not contain more than 5 genres")
    v.Check(validator.Unique(movie.Genres), "genres", "must not contain duplicate values")
}

重要提示:現(xiàn)在檢查是對(duì)一個(gè)movie結(jié)構(gòu)體實(shí)例各個(gè)字段進(jìn)行的,而不是對(duì)input結(jié)構(gòu)體牛曹。

完成上面的改造之后佛点,我們需要返回createMovieHandler并更新代碼,通過(guò)初始化一個(gè)新的Movie結(jié)構(gòu)體黎比,從input結(jié)構(gòu)體復(fù)制數(shù)據(jù)到movie結(jié)構(gòu)體中恋脚,然后調(diào)用這個(gè)新的驗(yàn)證函數(shù)。像這樣:

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
    }
    movie := &data.Movie{
        Title:   input.Title,
        Year:    input.Year,
        Runtime: input.Runtime,
        Genres:  input.Genres,
    }

        //初始化Validator實(shí)例
    v := validator.New()

    //調(diào)用ValidateMovie()函數(shù)焰手,如果有錯(cuò)誤就返回給客戶端糟描。
    if data.ValidateMovie(v, movie); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }
    fmt.Fprintf(w, "%+v\n", input)
}

當(dāng)您查看這些代碼時(shí),您的腦海中可能會(huì)有幾個(gè)問(wèn)題书妻。

首先船响,您可能想知道為什么我們?cè)谔幚沓绦蛑谐跏蓟疺alidator實(shí)例并將其傳遞給ValidateMovie()函數(shù)——而不是在ValidateMovie()中初始化它并將其作為返回值傳遞回來(lái)。

這是因?yàn)殡S著應(yīng)用程序變得越來(lái)越復(fù)雜躲履,我們將需要從處理程序調(diào)用多個(gè)校驗(yàn)幫助函數(shù)见间,而不是像上面所示的就一個(gè)。因此工猜,在處理程序中初始化Validator米诉,然后傳遞給幫助函數(shù),這給了我們更多的靈活性篷帅。

您可能還想知道史侣,為什么我們要將JSON請(qǐng)求解碼為input結(jié)構(gòu)體類型,然后復(fù)制數(shù)據(jù)魏身,而不是直接解碼為Movie結(jié)構(gòu)體實(shí)例惊橱。

因?yàn)閙ovie里面有些字段例如ID和Version是不需要客戶端提供的,如果使用movie的話箭昵,客戶端提供ID和Verison字段也會(huì)被解碼到movie結(jié)構(gòu)體中税朴,這就需要多余的檢查工作。

但是將客戶端的請(qǐng)求內(nèi)容解析到一個(gè)臨時(shí)的結(jié)構(gòu)體中家制,會(huì)更靈活正林,簡(jiǎn)潔而且代碼更健壯。

有了這些解釋颤殴,您應(yīng)該能夠再次啟動(dòng)應(yīng)用程序觅廓,并且從客戶端的角度來(lái)看,效果應(yīng)該與之前的一樣诅病。如果你發(fā)起一個(gè)無(wú)效的請(qǐng)求哪亿,你應(yīng)該會(huì)得到一個(gè)包含類似這樣的錯(cuò)誤消息的響應(yīng):

$ BODY='{"title":"","year":1000,"runtime":"-123 mins","genres":["sci-fi","sci-fi"]}' 
$ curl -i -d "$BODY" localhost:4000/v1/movies
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Date: Wed, 07 Apr 2021 10:33:57 GMT 
Content-Length: 180

{
    "error":
    {
        "genres": "must not contain duplicate values",
        "runtime": "must be a positive integer",
        "title": "must be provided",
        "year": "must be greater than 1888"
    }
}

您可以隨意測(cè)試粥烁,并嘗試在JSON中發(fā)送不同的值,直到所有的校驗(yàn)都按預(yù)期工作為止蝇棉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末讨阻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子篡殷,更是在濱河造成了極大的恐慌钝吮,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件板辽,死亡現(xiàn)場(chǎng)離奇詭異奇瘦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)劲弦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門耳标,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人邑跪,你說(shuō)我怎么就攤上這事次坡。” “怎么了画畅?”我有些...
    開(kāi)封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵砸琅,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我轴踱,道長(zhǎng)症脂,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任淫僻,我火速辦了婚禮诱篷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嘁傀。我一直安慰自己兴蒸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布细办。 她就那樣靜靜地躺著,像睡著了一般蕾殴。 火紅的嫁衣襯著肌膚如雪笑撞。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天钓觉,我揣著相機(jī)與錄音茴肥,去河邊找鬼。 笑死荡灾,一個(gè)胖子當(dāng)著我的面吹牛瓤狐,可吹牛的內(nèi)容都是我干的瞬铸。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼础锐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嗓节!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起皆警,我...
    開(kāi)封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拦宣,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后信姓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鸵隧,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年意推,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了豆瘫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡菊值,死狀恐怖靡羡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情俊性,我是刑警寧澤略步,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站定页,受9級(jí)特大地震影響趟薄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜典徊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一杭煎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卒落,春花似錦羡铲、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至腰湾,卻和暖如春雷恃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背费坊。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工倒槐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人附井。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓讨越,卻偏偏與公主長(zhǎng)得像两残,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子把跨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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