這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文同時發表在https://github.com/zhangyachen/zhangyachen.github.io/issues/125
假設我們有如下結構體:
type User struct { Id int Name string Bio string Email string }
我們需要對結構體內的欄位進行驗證合法性:
- Id的值在某一個範圍內。
- Name的長度在某一個範圍內。
- Email格式正確。
我們可能會這麼寫:
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}
這樣的話代碼比較冗餘,而且如果結構體新加欄位,還需要再修改驗證函式再加一段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。如果對這個比較陌生的話,看看下面這個:
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這個用法,json:xxx其實也是一個structTag,只不過這是golang幫你實現好特定用法的structTag。而validate:"number,min=1,max=1000"是我們自訂的structTag。
實現思路
我們定義一個介面Validator,定義一個方法Validate。再定義有具體意義的驗證器例如StringValidator、NumberValidator、EmailValidator來實現介面Validator。
這裡為什麼要使用介面?假設我們不使用介面代碼會怎麼寫?
if tagIsOfNumber(){ validator := NumberValidator{}}else if tagIsOfString() { validator := StringValidator{}}else if tagIsOfEmail() { validator := EmailValidator{}}else if tagIsOfDefault() { validator := DefaultValidator{}}
這樣的話判斷邏輯不能寫在一個函數中,因為傳回值validator會因為structTag的不同而不同,而且validator也不能當做函數參數做傳遞。而我們定義一個介面,所有的validator都去實現這個介面,上述的問題就能解決,而且邏輯更加清晰和緊湊。
關於介面的使用可以看下標準庫的io Writer,Writer是個interface,只有一個方法Writer:
type Writer interface { Write(p []byte) (n int, err error)}
而輸出函數可以直接調用參數的Write方法即可,無需關心到底是寫到檔案還是寫到標準輸出:
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) //調用Write方法 p.free() return}//調用Fprintf(os.Stdout, format, a...) //標準輸出Fprintf(os.Stderr, msg+"\n", args...) //標準錯誤輸出var buf bytes.BufferFprintf(&buf, "[") //寫入到Buffer的緩衝中
言歸正傳,我們看下完整代碼,代碼是Custom struct field tags in Golang中給出的:
package mainimport ( "fmt" "reflect" "regexp" "strings")const tagName = "validate"//郵箱驗證正則var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)//驗證介面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解析到結構體中 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()) }}
代碼很好理解,結構也很清晰,不做過多解釋了^_^
github上其實已經有現成的驗證封裝了govalidator,支援內建支援的驗證tag和自訂驗證tag:
package mainimport ( "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驗證函式 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) }}
參考資料:
- Custom struct field tags in Golang
- Data validation in Golang
- govalidator