golang自定義struct字段標簽


原文鏈接: https://sosedoff.com/2016/07/16/golang-struct-tags.html

struct是golang中最常使用的變量類型之一,幾乎每個地方都有使用,從處理配置選項到使用encoding/json或encoding/xml包編排JSON或XML文檔。字段標簽是struct字段定義部分,允許你使用優雅簡單的方式存儲許多用例字段的元數據(如字段映射,數據校驗,對象關系映射等等)。

基本原理

通常structs最讓人感興趣的是什么?strcut最有用的特征之一是能夠制定字段名映射。如果你處理外部服務並進行大量數據轉換它將非常方便。讓我們看下如下示例:

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"`
}

在User結構體中,標簽僅僅是字段類型定義后面用反引號封閉的字符串。在示例中我們重新定義字段名以便進行JSON編碼和反編碼。意即當對結構體字段進行JSON編碼,它將會使用用戶定義的字段名代替默認的大寫名字。下面是通過json.Marshal調用產生的沒有自定義標簽的結構體輸出:

{
  "Id": 1,
  "Name": "John Doe",
  "Bio": "Some Text",
  "Active": true,
  "Admin": false,
  "CreatedAt": "2016-07-16T15:32:17.957714799Z"
}

如你所見,示例中所有的字段輸出都與它們在User結構體中定義相關。現在,讓我們添加自定義JSON標簽,看會發生什么:

{
  "id": 1,
  "name": "John Doe",
  "about": "Some Text",
  "active": true,
  "created_at": "2016-07-16T15:32:17.957714799Z"
}

通過自定義標簽我們能夠重塑輸出。使用json:"-"定義我們告訴編碼器完全跳過該字段。查看JSON和XML包以獲取更多細節和可用的標簽選項。

自主研發

既然我們理解了結構體標簽是如何被定義和使用的,我們嘗試編寫自己的標簽處理器。為實現該功能我們需要檢查結構體並且讀取標簽屬性。這就需要用到reflect包。

假定我們要實現簡單的校驗庫,基於字段類型使用字段標簽定義一些校驗規則。我們常想要在將數據保存到數據庫之前對其進行校驗。

package main

import (
	"reflect"
	"fmt"
)

const tagName = "validate"

type User struct {
	Id int `validate:"-"`
	Name string `validate:"presence,min=2,max=32"`
	Email string `validate:"email,required"`
}

func main() {
	user := User{
		Id: 1,
		Name: "John Doe",
		Email: "john@example",
	}

	// TypeOf returns the reflection Type that represents the dynamic type of variable.
	// If variable is a nil interface value, TypeOf returns nil.
	t := reflect.TypeOf(user)

	//Get the type and kind of our user variable
	fmt.Println("Type: ", t.Name())
	fmt.Println("Kind: ", t.Kind())

	for i := 0; i < t.NumField(); i++ {
		// Get the field, returns https://golang.org/pkg/reflect/#StructField
		field := t.Field(i)

		//Get the field tag value
		tag := field.Tag.Get(tagName)

		fmt.Printf("%d. %v(%v), tag:'%v'\n", i+1, field.Name, field.Type.Name(), tag)
	}


}

輸出:

Type:  User
Kind:  struct
1. Id(int), tag:'-'
2. Name(string), tag:'presence,min=2,max=32'
3. Email(string), tag:'email,required'

通過reflect包我們能夠獲取User結構體id基本信息,包括它的類型、種類且能列出它的所有字段。如你所見,我們打印了每個字段的標簽。標簽沒有什么神奇的地方,field.Tag.Get方法返回與標簽名匹配的字符串,你可以自由使用做你想做的。

為向你說明如何使用結構體標簽進行校驗,我使用接口形式實現了一些校驗類型(numeric, string, email).下面是可運行的代碼示例:

package main

import (
	"regexp"
	"fmt"
	"strings"
	"reflect"
)

//Name of the struct tag used in example.
const tagName = "validate"

//Regular expression to validate email address.
var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)

//Generic data validator
type Validator interface {
	//Validate method performs validation and returns results and optional error.
	Validate(interface{})(bool, error)
}

//DefaultValidator does not perform any validations
type DefaultValidator struct{

}

func (v DefaultValidator) Validate(val interface{}) (bool, error) {
	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
}

//StringValidator validates string presence and/or its length
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 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
}

//Returns validator struct corresponding to validation type
func getValidatorFromTag(tag string) Validator {
	args := strings.Split(tag, ",")

	switch args[0] {
	case "number":
		validator := NumberValidator{}
		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{}
}

//Performs actual data validation using validator definitions on the struct
func validateStruct(s interface{}) []error {
	errs := []error{}

	//ValueOf returns a Value representing the run-time data
	v := reflect.ValueOf(s)

	for i := 0; i < v.NumField(); i++ {
		//Get the field tag value
		tag := v.Type().Field(i).Tag.Get(tagName)

		//Skip if tag is not defined or ignored
		if tag == "" || tag == "-" {
			continue
		}

		//Get a validator that corresponds to a tag
		validator := getValidatorFromTag(tag)

		//Perform validation
		valid, err := validator.Validate(v.Field(i).Interface())

		//Append error to results
		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:"string"`
}

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())
	}
}

輸出:

Errors: 
	1. Id should be greater than 1
	2. Name should be less than 10 chars long
	3. Bio cannot be blank
	4. Email should be less than 0 chars long
	

在User結構體我們定義了一個Id字段校驗規則,檢查值是否在合適范圍1-1000之間。Name字段值是一個字符串,校驗器應檢查其長度。Bio字段值是一個字符串,我們僅需其值不為空,不須校驗。最后,Email字段值應是一個合法的郵箱地址(至少是格式化的郵箱)。例中User結構體字段均非法,運行代碼將會獲得以下輸出:

Errors: 
	1. Id should be greater than 1
	2. Name should be less than 10 chars long
	3. Bio cannot be blank
	4. Email should be less than 0 chars long
	

最后一例與之前例子(使用類型的基本反射)的主要不同之處在於,我們使用reflect.ValueOf代替reflect.TypeOf。還需要使用v.Field(i).Interface()獲取字段值,該方法提供了一個接口,我們可以進行校驗。使用v.Type().Filed(i)我們還可以獲取字段類型。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM