原文链接: 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)我们还可以获取字段类型。

golang自定义struct字段标签的更多相关文章

  1. 夺命雷公狗---DEDECMS----11dedecms字段标签

    如果我们在开发的时候需要对获取的某个字段进行二次开发,我们可以对字段值调用某个函数来完成需求, 实例代码如下所示: <!DOCTYPE html> <html> <hea ...

  2. [转]Golang之struct类型

    http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=22312037&id=3756923 一.struct        ...

  3. DedeCMS织梦自定义图片字段调用出现{dede:img ..}

    做站过程中碰到这样一个问题,找到解决办法收藏分享:为什么在首页用自定义列表调用出来的图片字段不是正确的图片地址,而是类似于: {dede:img text='' width='270' height= ...

  4. 基于SSM3框架FreeMarker自定义指令(标签)实现

    通过之前的Spring MVC 3.0.5+Spring 3.0.5+MyBatis3.0.4全注解实例详解系列文章,我们已经成功的整合到了一起,这次大象将在此基础上对框架中的FreeMarker模板 ...

  5. Android自定义控件之自定义ViewGroup实现标签云

    前言: 前面几篇讲了自定义控件绘制原理Android自定义控件之基本原理(一),自定义属性Android自定义控件之自定义属性(二),自定义组合控件Android自定义控件之自定义组合控件(三),常言 ...

  6. jsp如何自定义tag的标签库?

    虽然和上一次的使用自定义的tld标签简化jsp的繁琐操作的有点不同,但是目的也是一致的.自定义tag比较简单. 1.新建tag标签 在WEB-INF目录下新建一个tags的文件夹,是自定义tag标签的 ...

  7. GoLang获取struct的tag

    GoLang获取struct的tag内容:beego的ORM中也通过tag来定义参数的. 获取tag的内容是利用反射包来实现的.示例代码能清楚的看懂! package main import ( &q ...

  8. ArcGIS自定义工具箱-字段合并

    ArcGIS自定义工具箱-字段合并 联系方式:谢老师,135-4855-4328,xiexiaokui#qq.com 目的:用指定字符合并两个字段 用例:湖南/长沙=>湖南省长沙市 数据源: 使 ...

  9. ArcGIS自定义工具箱-字段分割

    ArcGIS自定义工具箱-字段分割 联系方式:谢老师,135-4855-4328,xiexiaokui#qq.com 目的:用指定分割符分割字段, 用例:湖南省长沙市=>湖南/长沙 数据源: 使 ...

随机推荐

  1. C语言程序设计II—第八周教学

    第八周教学总结(15/4-21/4) 教学内容 本周的教学内容为: 8.4 电码加密 知识点:指针与字符串,重难点:字符指针与字符串的关联和区别: 8.5 任意个整数求和 知识点:动态内存分配的概念和 ...

  2. 3.0+百度地图在地图初始化的时候就弹框展示一个信息框,而不是用户点击poi时才弹出

    有些时候我们会有这样的业务:当用户根据地址跳转到地图后,希望能够立即弹框展示该poi的地址信息. 实现方法: 百度地图3.0+的类BaiduMap中提供了一个方法showInfoWindow(Info ...

  3. 开发工具|给你的项目买份保险:Python虚拟环境

    读完需要 9 分钟 1. 什么是虚拟环境? 虚拟环境的意义,就如同 虚拟机 一样,它可以实现不同环境中Python依赖包相互独立,互不干扰.这在一定程度的意义上,给了我们的项目一份很有力的保障.在这里 ...

  4. 推荐几本对于Python初学者比较好的书籍(内含PDF)

    我们提供一个初学者最好的Python书籍列表.Python是一个初级程序员可以学习编程的最友好语言之一.为了帮助您开始使用Python编程,我们分享此列表.泡一杯茶,选一本书阅读,开始使用Python ...

  5. 办公室的远程传文件 的命令三种方式linux

    不同的Linux之间copy文件常用有3种方法: 第一种就是ftp,也就是其中一台Linux安装ftp Server,这样可以另外一台使用ftp的client程序来进行文件的copy. 第二种方法就是 ...

  6. Redis命令总结 (转)

    Redis命令总结   连接操作相关的命令 quit:关闭连接(connection) auth:简单密码认证 持久化 save:将数据同步保存到磁盘 bgsave:将数据异步保存到磁盘 lastsa ...

  7. SRP周记_20190418

    SRP的全名是Student research project (我猜的),对于部分学生来说,不做就相当于不能毕业,一个project大概做一年左右,算是我工的一个特色.本人参加的SRP是做一个编译原 ...

  8. Luogu P2473 [SCOI2008]奖励关

    比较恶心的概率(期望)+状压DP,想正推2H的我瑟瑟发抖 由于数据范围不大,因此我们可以直接状压每个宝物取或不取的情况,设\(f_{i,j}\)表示前\(i\)轮且宝物是否取过的状态为\(j\)时的方 ...

  9. Luogu P4317 花神的数论题

    也是一道不错的数位DP,考虑先转成二进制后再做 转化一下问题,考虑统计出\([1,n]\)中在二进制下有\(i\)个\(1\)的方案数\(cnt_i\),那么答案显然就是\(\prod i^{cnt_ ...

  10. Luogu P1514 引水入城

    我承认我有点懒(洛谷已经发过题解了,但我发誓要坚持写博客) 这道题坑了我3天…… 首先一看就与染色问题类似,果断BFS(写DFS炸了) 先将最上面(靠近水)的一行全部扔进队列里,做一遍BFS 再对最下 ...