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

struct是golang中最常使用的变量类型之一,几乎每个地方都有使用,从处理配置选项到使用encoding/json或encoding/xml包编排JSON或XML文档。字段标签是struct字段定义部分,允许你使用优雅简单的方式存储许多用例字段的元数据(如字段映射,数据校验,对象关系映射等等)。

基本原理

通常structs最让人感兴趣的是什么?strcut最有用的特征之一是能够制定字段名映射。如果你处理外部服务并进行大量数据转换它将非常方便。让我们看下如下示例:

  1. type User struct {
  2. Id int `json:"id"`
  3. Name string `json:"name"`
  4. Bio string `json:"about,omitempty"`
  5. Active bool `json:"active"`
  6. Admin bool `json:"-"`
  7. CreatedAt time.Time `json:"created_at"`
  8. }

在User结构体中,标签仅仅是字段类型定义后面用反引号封闭的字符串。在示例中我们重新定义字段名以便进行JSON编码和反编码。意即当对结构体字段进行JSON编码,它将会使用用户定义的字段名代替默认的大写名字。下面是通过json.Marshal调用产生的没有自定义标签的结构体输出:

  1. {
  2. "Id": 1,
  3. "Name": "John Doe",
  4. "Bio": "Some Text",
  5. "Active": true,
  6. "Admin": false,
  7. "CreatedAt": "2016-07-16T15:32:17.957714799Z"
  8. }

如你所见,示例中所有的字段输出都与它们在User结构体中定义相关。现在,让我们添加自定义JSON标签,看会发生什么:

  1. {
  2. "id": 1,
  3. "name": "John Doe",
  4. "about": "Some Text",
  5. "active": true,
  6. "created_at": "2016-07-16T15:32:17.957714799Z"
  7. }

通过自定义标签我们能够重塑输出。使用json:"-"定义我们告诉编码器完全跳过该字段。查看JSON和XML包以获取更多细节和可用的标签选项。

自主研发

既然我们理解了结构体标签是如何被定义和使用的,我们尝试编写自己的标签处理器。为实现该功能我们需要检查结构体并且读取标签属性。这就需要用到reflect包。

假定我们要实现简单的校验库,基于字段类型使用字段标签定义一些校验规则。我们常想要在将数据保存到数据库之前对其进行校验。

  1. package main
  2. import (
  3. "reflect"
  4. "fmt"
  5. )
  6. const tagName = "validate"
  7. type User struct {
  8. Id int `validate:"-"`
  9. Name string `validate:"presence,min=2,max=32"`
  10. Email string `validate:"email,required"`
  11. }
  12. func main() {
  13. user := User{
  14. Id: 1,
  15. Name: "John Doe",
  16. Email: "john@example",
  17. }
  18. // TypeOf returns the reflection Type that represents the dynamic type of variable.
  19. // If variable is a nil interface value, TypeOf returns nil.
  20. t := reflect.TypeOf(user)
  21. //Get the type and kind of our user variable
  22. fmt.Println("Type: ", t.Name())
  23. fmt.Println("Kind: ", t.Kind())
  24. for i := 0; i < t.NumField(); i++ {
  25. // Get the field, returns https://golang.org/pkg/reflect/#StructField
  26. field := t.Field(i)
  27. //Get the field tag value
  28. tag := field.Tag.Get(tagName)
  29. fmt.Printf("%d. %v(%v), tag:'%v'\n", i+1, field.Name, field.Type.Name(), tag)
  30. }
  31. }

输出:

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

通过reflect包我们能够获取User结构体id基本信息,包括它的类型、种类且能列出它的所有字段。如你所见,我们打印了每个字段的标签。标签没有什么神奇的地方,field.Tag.Get方法返回与标签名匹配的字符串,你可以自由使用做你想做的。

为向你说明如何使用结构体标签进行校验,我使用接口形式实现了一些校验类型(numeric, string, email).下面是可运行的代码示例:

  1. package main
  2. import (
  3. "regexp"
  4. "fmt"
  5. "strings"
  6. "reflect"
  7. )
  8. //Name of the struct tag used in example.
  9. const tagName = "validate"
  10. //Regular expression to validate email address.
  11. var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)
  12. //Generic data validator
  13. type Validator interface {
  14. //Validate method performs validation and returns results and optional error.
  15. Validate(interface{})(bool, error)
  16. }
  17. //DefaultValidator does not perform any validations
  18. type DefaultValidator struct{
  19. }
  20. func (v DefaultValidator) Validate(val interface{}) (bool, error) {
  21. return true, nil
  22. }
  23. type NumberValidator struct{
  24. Min int
  25. Max int
  26. }
  27. func (v NumberValidator) Validate(val interface{}) (bool, error) {
  28. num := val.(int)
  29. if num < v.Min {
  30. return false, fmt.Errorf("should be greater than %v", v.Min)
  31. }
  32. if v.Max >= v.Min && num > v.Max {
  33. return false, fmt.Errorf("should be less than %v", v.Max)
  34. }
  35. return true, nil
  36. }
  37. //StringValidator validates string presence and/or its length
  38. type StringValidator struct {
  39. Min int
  40. Max int
  41. }
  42. func (v StringValidator) Validate(val interface{}) (bool, error) {
  43. l := len(val.(string))
  44. if l == 0 {
  45. return false, fmt.Errorf("cannot be blank")
  46. }
  47. if l < v.Min {
  48. return false, fmt.Errorf("should be at least %v chars long", v.Min)
  49. }
  50. if v.Max >= v.Min && l > v.Max {
  51. return false, fmt.Errorf("should be less than %v chars long", v.Max)
  52. }
  53. return true, nil
  54. }
  55. type EmailValidator struct{
  56. }
  57. func (v EmailValidator) Validate(val interface{}) (bool, error) {
  58. if !mailRe.MatchString(val.(string)) {
  59. return false, fmt.Errorf("is not a valid email address")
  60. }
  61. return true, nil
  62. }
  63. //Returns validator struct corresponding to validation type
  64. func getValidatorFromTag(tag string) Validator {
  65. args := strings.Split(tag, ",")
  66. switch args[0] {
  67. case "number":
  68. validator := NumberValidator{}
  69. fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
  70. return validator
  71. case "string":
  72. validator := StringValidator{}
  73. fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
  74. return validator
  75. case "email":
  76. return EmailValidator{}
  77. }
  78. return DefaultValidator{}
  79. }
  80. //Performs actual data validation using validator definitions on the struct
  81. func validateStruct(s interface{}) []error {
  82. errs := []error{}
  83. //ValueOf returns a Value representing the run-time data
  84. v := reflect.ValueOf(s)
  85. for i := 0; i < v.NumField(); i++ {
  86. //Get the field tag value
  87. tag := v.Type().Field(i).Tag.Get(tagName)
  88. //Skip if tag is not defined or ignored
  89. if tag == "" || tag == "-" {
  90. continue
  91. }
  92. //Get a validator that corresponds to a tag
  93. validator := getValidatorFromTag(tag)
  94. //Perform validation
  95. valid, err := validator.Validate(v.Field(i).Interface())
  96. //Append error to results
  97. if !valid && err != nil {
  98. errs = append(errs, fmt.Errorf("%s %s", v.Type().Field(i).Name, err.Error()))
  99. }
  100. }
  101. return errs
  102. }
  103. type User struct {
  104. Id int `validate:"number,min=1,max=1000"`
  105. Name string `validate:"string,min=2,max=10"`
  106. Bio string `validate:"string"`
  107. Email string `validate:"string"`
  108. }
  109. func main() {
  110. user := User{
  111. Id: 0,
  112. Name: "superlongstring",
  113. Bio: "",
  114. Email: "foobar",
  115. }
  116. fmt.Println("Errors: ")
  117. for i, err := range validateStruct(user) {
  118. fmt.Printf("\t%d. %s\n", i+1, err.Error())
  119. }
  120. }

输出:

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

在User结构体我们定义了一个Id字段校验规则,检查值是否在合适范围1-1000之间。Name字段值是一个字符串,校验器应检查其长度。Bio字段值是一个字符串,我们仅需其值不为空,不须校验。最后,Email字段值应是一个合法的邮箱地址(至少是格式化的邮箱)。例中User结构体字段均非法,运行代码将会获得以下输出:

  1. Errors:
  2. 1. Id should be greater than 1
  3. 2. Name should be less than 10 chars long
  4. 3. Bio cannot be blank
  5. 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. DRF跨域,简单请求和复杂请求

    跨域就是跨域名,跨端口 - 为什么会有跨域? 浏览器有同源限制策略 - 绕过浏览器同源策略就可以跨域 -  方式一: jsonp(利用浏览器特性) 在html动态创建script标签 同源策略会阻止a ...

  2. MySQL 基础十 性能优化

    1.优化sql 2.建立索引 3.优化表结构,避免多个join查询

  3. WireShark抓包工具使用

    WireShark是一款网络封包分析软件,它抓取网络封包,并尽可能显示出最详细的封包资料. wireshark的准备工作 安装wireshark sudo apt-get install wiresh ...

  4. 随机指定范围内N个不重复的数

    此为工具类,支持抽奖业务需求,具体实现见下方代码: package com.org.test; import java.util.ArrayList; import java.util.List; p ...

  5. TravelPort官方API解读

    TravelPort Ping通使用教程 Unit1 Lesson 1: 标签(空格分隔): 完成第1单元的三个课程后,您可以使用Travelport Universal API来提出服务请求并了解响 ...

  6. Mapreduce打印调试输出

    Mapreduce打印调试内容: 一.启动JobHistoryServer mr-jobhistory-daemon.sh start historyserver [hadoop@node11 sbi ...

  7. odoo订餐系统之类型设计

    这次开发的模块是订餐的类型设计,比如大荤 小荤 蔬菜 米饭 等基本数据.1.设计model类,很简单就一个字段: class MyLunchProductionCategory(osv.Model): ...

  8. AT2134 Zigzag MST

    题面 题解 这个题目主要是连边很奇怪,但是我们可以发现一个性质:权值是递增的. 于是像下图的连边:(加边方式为\((A_1, B_1, 1)\)) 其实可以等价于如下连边: 于是我们将其变成了在环上连 ...

  9. 扩展ASP.NET Identity使用Int做主键

    当我们默认新建一个ASP.NET MVC项目的时候,使用的身份认证系统是ASP.NET Identity.但是这里的Identity使用的主键为String类型的GUID.当然这是大多数系统首先类型. ...

  10. 【数据库】Mysql中主键的几种表设计组合的实际应用效果

    写在前面 前前后后忙忙碌碌,度过了新工作的三个月.博客许久未新,似乎对忙碌没有一点点防备.总结下来三个月不断的磨砺自己,努力从独乐乐转变到众乐乐,体会到不一样的是,连办公室的新玩意都能引起莫名的兴趣了 ...