boss: 這小子還不會(huì)使用validator庫進(jìn)行數(shù)據(jù)校驗(yàn),開了~
前言
在公司做項(xiàng)目,在做API部分開發(fā)時(shí),需要對(duì)請(qǐng)求參數(shù)的校驗(yàn),防止用戶的惡意請(qǐng)求。例如日期格式,用戶年齡,性別等必須是正常的值,不能隨意設(shè)置。最開始在做這一部分的時(shí)候,我采用老方法,自己編寫參數(shù)檢驗(yàn)方法,統(tǒng)一進(jìn)行參數(shù)驗(yàn)證。后來在同事CR的時(shí)候,說GIN有更好的參數(shù)檢驗(yàn)方法,gin框架使用github.com/go-playground/validator進(jìn)行參數(shù)校驗(yàn),我們只需要在定義結(jié)構(gòu)體時(shí)使用binding或validatetag標(biāo)識(shí)相關(guān)校驗(yàn)規(guī)則,就可以進(jìn)行參數(shù)校驗(yàn)了,很方便。相信也有很多小伙伴不知道這個(gè)功能,今天就來介紹一下這部分。
快速安裝
使用之前,我們先要獲取validator這個(gè)庫。
# 第一次安裝使用如下命令
$ go get github.com/go-playground/validator/v10
# 項(xiàng)目中引入包
import "github.com/go-playground/validator/v10"
簡單示例
安裝還是很簡單的,下面我先來一個(gè)官方樣例,看看是怎么使用的,然后展開分析。
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Nickname string `json:"nickname" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
Age uint8 `json:"age" binding:"gte=1,lte=120"`
}
func main() {
router := gin.Default()
router.POST("register", Register)
router.Run(":9999")
}
func Register(c *gin.Context) {
var r RegisterRequest
err := c.ShouldBindJSON(&r)
if err != nil {
fmt.Println("register failed")
c.JSON(http.StatusOK, gin.H{"msg": err.Error()})
return
}
//驗(yàn)證 存儲(chǔ)操作省略.....
fmt.Println("register success")
c.JSON(http.StatusOK, "successful")
}
測試
curl --location --request POST 'http://localhost:9999/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"username": "asong",
"nickname": "golang夢(mèng)工廠",
"email": "7418.com",
"password": "123",
"age": 140
}'
返回結(jié)果
{
"msg": "Key: 'RegisterRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag\nKey: 'RegisterRequest.Age' Error:Field validation for 'Age' failed on the 'lte' tag"
}
看這個(gè)輸出結(jié)果,我們可以看到validator的檢驗(yàn)生效了,email字段不是一個(gè)合法郵箱,age字段超過了最大限制。我們只在結(jié)構(gòu)體中添加tag就解決了這個(gè)問題,是不是很方便,下面我們就來學(xué)習(xí)一下具體使用。
validator庫
gin框架是使用validator.v10這個(gè)庫來進(jìn)行參數(shù)驗(yàn)證的,所以我們先來看看這個(gè)庫的使用。
先安裝這個(gè)庫:
$ go get github.com/go-playground/validator/v10
然后先寫一個(gè)簡單的示例:
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type User struct {
Username string `validate:"min=6,max=10"`
Age uint8 `validate:"gte=1,lte=10"`
Sex string `validate:"oneof=female male"`
}
func main() {
validate := validator.New()
user1 := User{Username: "asong", Age: 11, Sex: "null"}
err := validate.Struct(user1)
if err != nil {
fmt.Println(err)
}
user2 := User{Username: "asong111", Age: 8, Sex: "male"}
err = validate.Struct(user2)
if err != nil {
fmt.Println(err)
}
}
我們?cè)诮Y(jié)構(gòu)體定義validator標(biāo)簽的tag,使用validator.New()創(chuàng)建一個(gè)驗(yàn)證器,這個(gè)驗(yàn)證器可以指定選項(xiàng)、添加自定義約束,然后在調(diào)用他的Struct()方法來驗(yàn)證各種結(jié)構(gòu)對(duì)象的字段是否符合定義的約束。
上面的例子,我們?cè)赨ser結(jié)構(gòu)體中,有三個(gè)字段:
Name:通過min和max來進(jìn)行約束,Name的字符串長度為[6,10]之間。 Age:通過gte和lte對(duì)年輕的范圍進(jìn)行約束,age的大小大于1,小于10。 Sex:通過oneof對(duì)值進(jìn)行約束,只能是所列舉的值,oneof列舉出性別為男士??和女士??(不是硬性規(guī)定奧,可能還有別的性別)。
所以user1會(huì)進(jìn)行報(bào)錯(cuò),錯(cuò)誤信息如下:
Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tag
Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
Key: 'User.Sex' Error:Field validation for 'Sex' failed on the 'oneof' tag
各個(gè)字段違反了什么約束,一眼我們便能從錯(cuò)誤信息中看出來??赐炅撕唵问纠?,下面我就來看一看都有哪些tag,我們都可以怎么使用。本文不介紹所有的tag,更多使用方法,請(qǐng)到官方文檔自行學(xué)習(xí)。
字符串約束
excludesall:不包含參數(shù)中任意的 UNICODE 字符,例如excludesall=ab;excludesrune:不包含參數(shù)表示的 rune 字符,excludesrune=asong;startswith:以參數(shù)子串為前綴,例如startswith=hi;endswith:以參數(shù)子串為后綴,例如endswith=bye。contains=:包含參數(shù)子串,例如contains=email;containsany:包含參數(shù)中任意的 UNICODE 字符,例如containsany=ab;containsrune:包含參數(shù)表示的 rune 字符,例如`containsrune=asong;excludes:不包含參數(shù)子串,例如excludes=email;
范圍約束
范圍約束的字段類型分為三種:
對(duì)于數(shù)值,我們則可以約束其值 對(duì)于切片、數(shù)組和map,我們則可以約束其長度 對(duì)于字符串,我們則可以約束其長度
常用tag介紹:
ne:不等于參數(shù)值,例如ne=5;gt:大于參數(shù)值,例如gt=5;gte:大于等于參數(shù)值,例如gte=50;lt:小于參數(shù)值,例如lt=50;lte:小于等于參數(shù)值,例如lte=50;oneof:只能是列舉出的值其中一個(gè),這些值必須是數(shù)值或字符串,以空格分隔,如果字符串中有空格,將字符串用單引號(hào)包圍,例如oneof=male female。eq:等于參數(shù)值,注意與len不同。對(duì)于字符串,eq約束字符串本身的值,而len約束字符串長度。例如eq=10;len:等于參數(shù)值,例如len=10;max:小于等于參數(shù)值,例如max=10;min:大于等于參數(shù)值,例如min=10
Fields約束
eqfield:定義字段間的相等約束,用于約束同一結(jié)構(gòu)體中的字段。例如:eqfield=Passwordeqcsfield:約束統(tǒng)一結(jié)構(gòu)體中字段等于另一個(gè)字段(相對(duì)),確認(rèn)密碼時(shí)可以使用,例如:eqfiel=ConfirmPasswordnefield:用來約束兩個(gè)字段是否相同,確認(rèn)兩種顏色是否一致時(shí)可以使用,例如:nefield=Color1necsfield:約束兩個(gè)字段是否相同(相對(duì))
常用約束
unique:指定唯一性約束,不同類型處理不同:對(duì)于map,unique約束沒有重復(fù)的值 對(duì)于數(shù)組和切片,unique沒有重復(fù)的值 對(duì)于元素類型為結(jié)構(gòu)體的碎片,unique約束結(jié)構(gòu)體對(duì)象的某個(gè)字段不重復(fù),使用 unique=field指定字段名email:使用email來限制字段必須是郵件形式,直接寫eamil即可,無需加任何指定。omitempty:字段未設(shè)置,則忽略-:跳過該字段,不檢驗(yàn);|:使用多個(gè)約束,只需要滿足其中一個(gè),例如rgb|rgba;required:字段必須設(shè)置,不能為默認(rèn)值;
好啦,就介紹這些常用的約束,更多約束學(xué)習(xí)請(qǐng)到文檔自行學(xué)習(xí)吧,都有example供你學(xué)習(xí),很快的。
gin中的參數(shù)校驗(yàn)
學(xué)習(xí)了validator,我們也就知道了怎么在gin中使用參數(shù)校驗(yàn)了。這些約束是都沒有變的,在validator中,我們直接結(jié)構(gòu)體中將約束放到validate tag中,同樣道理,在gin中我們只需將約束放到bindingtag中就可以了。是不是很簡單。
但是有些時(shí)候,并不是所有的參數(shù)校驗(yàn)都能滿足我們的需求,所以我們可以定義自己的約束。自定義約束支持自定義結(jié)構(gòu)體校驗(yàn)、自定義字段校驗(yàn)等。這里來介紹一下自定義結(jié)構(gòu)體校驗(yàn)。
自定義結(jié)構(gòu)體校驗(yàn)
當(dāng)涉及到一些復(fù)雜的校驗(yàn)規(guī)則,這些已有的校驗(yàn)規(guī)則就不能滿足我們的需求了。例如現(xiàn)在有一個(gè)需求,存在db的用戶信息中創(chuàng)建時(shí)間與更新時(shí)間都要大于某一時(shí)間,假設(shè)是從前端傳來的(當(dāng)然不可能,哈哈)。現(xiàn)在我們來寫一個(gè)簡單示例,學(xué)習(xí)一下怎么對(duì)這個(gè)參數(shù)進(jìn)行校驗(yàn)。
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
type Info struct {
CreateTime time.Time `form:"create_time" binding:"required,timing" time_format:"2006-01-02"`
UpdateTime time.Time `form:"update_time" binding:"required,timing" time_format:"2006-01-02"`
}
// 自定義驗(yàn)證規(guī)則斷言
func timing(fl validator.FieldLevel) bool {
if date, ok := fl.Field().Interface().(time.Time); ok {
today := time.Now()
if today.After(date) {
return false
}
}
return true
}
func main() {
route := gin.Default()
// 注冊(cè)驗(yàn)證
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
err := v.RegisterValidation("timing", timing)
if err != nil {
fmt.Println("success")
}
}
route.GET("/time", getTime)
route.Run(":8080")
}
func getTime(c *gin.Context) {
var b Info
// 數(shù)據(jù)模型綁定查詢字符串驗(yàn)證
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "time are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
寫好了,下面我就來測試驗(yàn)證一下:
$ curl "localhost:8080/time?create_time=2020-10-11&update_time=2020-10-11"
# 結(jié)果
{"message":"time are valid!"}%
$ curl "localhost:8080/time?create_time=1997-10-11&update_time=1997-10-11"
# 結(jié)果
{"error":"Key: 'Info.CreateTime' Error:Field validation for 'CreateTime' failed on the 'timing' tag\nKey: 'Info.UpdateTime' Error:Field validation for 'UpdateTime' failed on the 'timing' tag"}%
這里我們看到雖然參數(shù)驗(yàn)證成功了,但是這里返回的錯(cuò)誤顯示的也太全了,在項(xiàng)目開發(fā)中不可以給前端返回這么詳細(xì)的信息的,所以我們需要改造一下:
func getTime(c *gin.Context) {
var b Info
// 數(shù)據(jù)模型綁定查詢字符串驗(yàn)證
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "time are valid!"})
} else {
_, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 1000, "msg": "param is error"})
}
}
這里在出現(xiàn)錯(cuò)誤時(shí)返回固定錯(cuò)誤即可。這里你也可以使用一個(gè)方法封裝一下,對(duì)錯(cuò)誤進(jìn)行處理在進(jìn)行返回,更多使用方法等你發(fā)覺喲。
小彩蛋
我們返回錯(cuò)誤時(shí)都是英文的,當(dāng)錯(cuò)誤很長的時(shí)候,對(duì)于我這種英語渣渣,就要借助翻譯軟件了。所以要是能返回的錯(cuò)誤直接是中文的就好了。validator庫本身是支持國際化的,借助相應(yīng)的語言包可以實(shí)現(xiàn)校驗(yàn)錯(cuò)誤提示信息的自動(dòng)翻譯。下面就寫一個(gè)代碼演示一下啦。
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslations "github.com/go-playground/validator/v10/translations/en"
chTranslations "github.com/go-playground/validator/v10/translations/zh"
)
var trans ut.Translator
// loca 通常取決于 http 請(qǐng)求頭的 'Accept-Language'
func transInit(local string) (err error) {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
zhT := zh.New() //chinese
enT := en.New() //english
uni := ut.New(enT, zhT, enT)
var o bool
trans, o = uni.GetTranslator(local)
if !o {
return fmt.Errorf("uni.GetTranslator(%s) failed", local)
}
//register translate
// 注冊(cè)翻譯器
switch local {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, trans)
case "zh":
err = chTranslations.RegisterDefaultTranslations(v, trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}
type loginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,max=16,min=6"`
}
func main() {
if err := transInit("zh"); err != nil {
fmt.Printf("init trans failed, err:%v\n", err)
return
}
router := gin.Default()
router.POST("/user/login", login)
err := router.Run(":8888")
if err != nil {
log.Println("failed")
}
}
func login(c *gin.Context) {
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
// 獲取validator.ValidationErrors類型的errors
errs, ok := err.(validator.ValidationErrors)
if !ok {
// 非validator.ValidationErrors類型錯(cuò)誤直接返回
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
// validator.ValidationErrors類型錯(cuò)誤則進(jìn)行翻譯
c.JSON(http.StatusOK, gin.H{
"msg": errs.Translate(trans),
})
return
}
//login 操作省略
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
})
}
我這里請(qǐng)求參數(shù)中限制密碼的長度,來驗(yàn)證一下吧。
curl --location --request POST 'http://localhost:8888/user/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"username": "asong",
"password": "11122222222222222222"
}'
# 返回
{
"msg": {
"loginRequest.Password": "Password長度不能超過16個(gè)字符"
}
}
看,直接顯示中文了,是不是很棒,我們可以在測試的時(shí)候使用這個(gè),上線項(xiàng)目不建議使用呦?。。?/p>
總結(jié)
好啦,這一篇文章到這里結(jié)束啦。這一篇干貨還是滿滿的。學(xué)會(huì)這些知識(shí)點(diǎn),提高我們的開發(fā)效率,省去了一些沒必要寫的代碼。能用的輪子我們還是不要錯(cuò)過滴。
推薦閱讀
