zoukankan      html  css  js  c++  java
  • 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'
    ", 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-zd-]+(.[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("	%d. %s
    ", 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)我们还可以获取字段类型。

  • 相关阅读:
    前导问题word使用技巧解决Word 生成目录时前导符不一致的问题(即通常所谓的目录中省略号大小不一致)
    安装用户debian7安装oracle11g
    字节文件MP3格式音频文件结构解析
    Linux下硬盘分区的最佳方案
    802.1x客户端软件 2.4版破解支持多网卡
    z9jpz.dll、gq0aku0.exe、cms2cmw.sys病毒
    Ghost批处理文件的基本格式
    利用ASP远程注册DLL的方法
    dllhost.exe系统进程介绍
    Unicode 和多字节字符集 (MBCS)
  • 原文地址:https://www.cnblogs.com/ycyoes/p/8416154.html
Copyright © 2011-2022 走看看