golang 如何驗(yàn)證struct字段的數(shù)據(jù)格式
網(wǎng)上看到一個(gè)字段驗(yàn)證的方法記錄一下方便以后再用
原文地址http://mp.weixin.qq.com/s/4Md7yWFyZLYKp68snsFHbw
假設(shè)我們有如下結(jié)構(gòu)體:
type User struct {
Id int
Name string
Bio string
Email string
}
我們需要對(duì)結(jié)構(gòu)體內(nèi)的字段進(jìn)行驗(yàn)證合法性:
? Id的值在某一個(gè)范圍內(nèi)。
? Name的長(zhǎng)度在某一個(gè)范圍內(nèi)。
? Email格式正確。
我們可能會(huì)這么寫:
user := User{
Id: 0,
Name: "superlongstring",
Bio: "",
Email: "foobar",
}
if user.Id < 1 && user.Id > 1000 {
return false
}
if len(user.Name) < 2 && len(user.Name) > 10 {
return false
}
if !validateEmail(user.Email) {
return false
}
這樣的話代碼比較冗余,而且如果結(jié)構(gòu)體新加字段,還需要再修改驗(yàn)證函數(shù)再加一段if判斷。這樣代碼比較冗余。我們可以借助golang的structTag來解決上述的問題:
type User struct {
Id int `validate:"number,min=1,max=1000"`
Name string `validate:"string,min=2,max=10"`
Bio string `validate:"string"`
Email string `validate:"email"`
}
validate:"number,min=1,max=1000"就是structTag。如果對(duì)這個(gè)比較陌生的話,看看下面這個(gè):
type User struct {
Id int `json:"id"`
Name string `json:"name"`
Bio string `json:"about,omitempty"`
Active bool `json:"active"`
Admin bool `json:"-"`
CreatedAt time.Time `json:"created_at"`
}
寫過golang的基本都用過json:xxx這個(gè)用法,json:xxx其實(shí)也是一個(gè)structTag,只不過這是golang幫你實(shí)現(xiàn)好特定用法的structTag。而validate:"number,min=1,max=1000"是我們自定義的structTag。
實(shí)現(xiàn)思路
我們定義一個(gè)接口Validator,定義一個(gè)方法Validate。再定義有具體意義的驗(yàn)證器例如StringValidator、NumberValidator、EmailValidator來實(shí)現(xiàn)接口Validator。
這里為什么要使用接口?假設(shè)我們不使用接口代碼會(huì)怎么寫?
if tagIsOfNumber(){
validator := NumberValidator{}
}else if tagIsOfString() {
validator := StringValidator{}
}else if tagIsOfEmail() {
validator := EmailValidator{}
}else if tagIsOfDefault() {
validator := DefaultValidator{}
}
這樣的話判斷邏輯不能寫在一個(gè)函數(shù)中,因?yàn)榉祷刂祐alidator會(huì)因?yàn)閟tructTag的不同而不同,而且validator也不能當(dāng)做函數(shù)參數(shù)做傳遞。而我們定義一個(gè)接口,所有的validator都去實(shí)現(xiàn)這個(gè)接口,上述的問題就能解決,而且邏輯更加清晰和緊湊。
關(guān)于接口的使用可以看下標(biāo)準(zhǔn)庫的io Writer,Writer是個(gè)interface,只有一個(gè)方法Writer:
type Writer interface {
Write(p []byte) (n int, err error)
}
而輸出函數(shù)可以直接調(diào)用參數(shù)的Write方法即可,無需關(guān)心到底是寫到文件還是寫到標(biāo)準(zhǔn)輸出:
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf) //調(diào)用Write方法
p.free()
return
}
//調(diào)用
Fprintf(os.Stdout, format, a...) //標(biāo)準(zhǔn)輸出
Fprintf(os.Stderr, msg+"\n", args...) //標(biāo)準(zhǔn)錯(cuò)誤輸出
var buf bytes.Buffer
Fprintf(&buf, "[") //
言歸正傳,我們看下完整代碼,代碼是 Custom struct field tags in Golang 中給出的:
package main
import (
"fmt"
"reflect"
"regexp"
"strings"
)
const tagName = "validate"
//郵箱驗(yàn)證正則
var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)
//驗(yàn)證接口
type Validator interface {
Validate(interface{}) (bool, error)
}
type DefaultValidator struct {
}
func (v DefaultValidator) Validate(val interface{}) (bool, error) {
return true, nil
}
type StringValidator struct {
Min int
Max int
}
func (v StringValidator) Validate(val interface{}) (bool, error) {
l := len(val.(string))
if l == 0 {
return false, fmt.Errorf("cannot be blank")
}
if l < v.Min {
return false, fmt.Errorf("should be at least %v chars long", v.Min)
}
if v.Max >= v.Min && l > v.Max {
return false, fmt.Errorf("should be less than %v chars long", v.Max)
}
return true, nil
}
type NumberValidator struct {
Min int
Max int
}
func (v NumberValidator) Validate(val interface{}) (bool, error) {
num := val.(int)
if num < v.Min {
return false, fmt.Errorf("should be greater than %v", v.Min)
}
if v.Max >= v.Min && num > v.Max {
return false, fmt.Errorf("should be less than %v", v.Max)
}
return true, nil
}
type EmailValidator struct {
}
func (v EmailValidator) Validate(val interface{}) (bool, error) {
if !mailRe.MatchString(val.(string)) {
return false, fmt.Errorf("is not a valid email address")
}
return true, nil
}
func getValidatorFromTag(tag string) Validator {
args := strings.Split(tag, ",")
switch args[0] {
case "number":
validator := NumberValidator{}
//將structTag中的min和max解析到結(jié)構(gòu)體中
fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
return validator
case "string":
validator := StringValidator{}
fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
return validator
case "email":
return EmailValidator{}
}
return DefaultValidator{}
}
func validateStruct(s interface{}) []error {
errs := []error{}
v := reflect.ValueOf(s)
for i := 0; i < v.NumField(); i++ {
//利用反射獲取structTag
tag := v.Type().Field(i).Tag.Get(tagName)
if tag == "" || tag == "-" {
continue
}
validator := getValidatorFromTag(tag)
valid, err := validator.Validate(v.Field(i).Interface())
if !valid && err != nil {
errs = append(errs, fmt.Errorf("%s %s", v.Type().Field(i).Name, err.Error()))
}
}
return errs
}
type User struct {
Id int `validate:"number,min=1,max=1000"`
Name string `validate:"string,min=2,max=10"`
Bio string `validate:"string"`
Email string `validate:"email"`
}
func main() {
user := User{
Id: 0,
Name: "superlongstring",
Bio: "",
Email: "foobar",
}
fmt.Println("Errors:")
for i, err := range validateStruct(user) {
fmt.Printf("\t%d. %s\n", i+1, err.Error())
}
}
代碼很好理解,結(jié)構(gòu)也很清晰,不做過多解釋了_
github上其實(shí)已經(jīng)有現(xiàn)成的驗(yàn)證包了govalidator,支持內(nèi)置支持的驗(yàn)證tag和自定義驗(yàn)證tag:
package main
import (
"github.com/asaskevich/govalidator"
"fmt"
"strings"
)
type Server struct {
ID string `valid:"uuid,required"`
Name string `valid:"machine_id"`
HostIP string `valid:"ip"`
MacAddress string `valid:"mac,required"`
WebAddress string `valid:"url"`
AdminEmail string `valid:"email"`
}
func main() {
server := &Server{
ID: "123e4567-e89b-12d3-a456-426655440000",
Name: "IX01",
HostIP: "127.0.0.1",
MacAddress: "01:23:45:67:89:ab",
WebAddress: "www.example.com",
AdminEmail: "admin@exmaple.com",
}
//自定義tag驗(yàn)證函數(shù)
govalidator.TagMap["machine_id"] = govalidator.Validator(func(str string) bool {
return strings.HasPrefix(str, "IX")
})
if ok, err := govalidator.ValidateStruct(server); err != nil {
panic(err)
} else {
fmt.Printf("OK: %v\n", ok)
}
}