Go 语言编程 — reflect 反射工具

末鹿安然 提交于 2020-08-20 04:45:09

目录

反射机制

反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行文件。因为可执行文件不存在变量名,所以在程序运行时,就无法通过变量名作为句柄并获取到自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如:结构体信息、字段名称、类型信息等整合到可执行文件中,并给程序提供接口访问这些反射信息,这样就可以在程序运行期间通过接口来获取到变量的反射信息,并且有能力修改它们。

reflect 包

Struct Tags 与 Reflect

举一个例子:Golang 中的变量名是大小写敏感的,对于小写的局部变量,在包外无法被引用。但实际上,Golang 程序是经常需要与其它系统进行数据交互的,例如:Redis 数据库访问。这样就需要一种设计,可以灵活的将 Golang 最核心的数据类型 Struct 转换为其他数据格式,这就是 Struct Tags。在需要将 Struct 转换为其它数据格式时,会根据 Tags 中定义的信息进行转换。

Struct Tags 类似注释,使用反引号 “`”,Golang 提供了 reflect 包来对 Struct Tags 进行解析。

示例:

package main

import (
    "fmt"
    "reflect"
)

type employee struct {
    ID       int     `json:"id"`
    Name     string  `json:"名字" validate:"presence,min=2,max=40"`
    Age      int     `json:"年龄"`
    Desc     string  `json:"描述" back:"好看否"`
    weight   float64 `json:"weight" 单位:"kg"`
    Salary   float64 `json:"-"`
    Email    string  `validate:"email,required"`
    MateName string  `json:"mate_name,omitempty"`
}

func main() {

    zhangsan := employee{
        ID:       1,
        Name:     "张三",
        Age:      18,
        Desc:     "秀色可餐",
        weight:   48.0,
        Salary:   12.0,
        MateName: "Prince",
    }

    t := reflect.TypeOf(zhangsan)
    fmt.Println("Type: ", t.Name())
    fmt.Println("Kind: ", t.Kind())
    fmt.Println("Num: ", t.NumField())

    tagName := "validate"
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        tag := field.Tag.Get(tagName)
        fmt.Printf("%d. %v(%v), tag:'%v'\n", i+1, field.Name, field.Type.Name(), tag)
    }
}

结果:

Type:  employee
Kind:  struct
Num:  8
1. ID(int), tag:''
2. Name(string), tag:'presence,min=2,max=40'
3. Age(int), tag:''
4. Desc(string), tag:''
5. weight(float64), tag:''
6. Salary(float64), tag:''
7. Email(string), tag:'email,required'
8. MateName(string), tag:''

通过 reflect 包,程序能够获取结构体的基本信息,包括它的成员清单、成员名称和成员类型。调用 field.Tag.Get 方法可以返回与 tagName 名匹配的 Struct Tags 的字符串,基于此我们就可以自由地去实现想要的逻辑了。

例如 json.Marshal 方法就有使用到反射机制,来完成 Struct 和 JSON 之间的数据格式转换。

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type employee struct {
    ID       int     `json:"id"`
    Name     string  `json:"名字" validate:"presence,min=2,max=40"`
    Age      int     `json:"年龄"`
    Desc     string  `json:"描述" back:"好看否"`
    weight   float64 `json:"weight" 单位:"kg"`
    Salary   float64 `json:"-"`
    Email    string  `validate:"email,required"`
    MateName string  `json:"mate_name,omitempty"`
}

func main() {
    zhangsan := employee{
        ID:       1,
        Name:     "张三",
        Age:      18,
        Desc:     "秀色可餐",
        weight:   48.0,
        Salary:   12.0,
        MateName: "Prince",
    }

    fmt.Println(zhangsan)
    re, _ := json.Marshal(zhangsan)
    fmt.Println(string(re))
}

结果:

$ go run tst.go
{1 张三 18 秀色可餐 48 12  Prince}
{"id":1,"名字":"张三","年龄":18,"描述":"秀色可餐","Email":"","mate_name":"Prince"}

其中,有以下要点需要注意

  1. 如果结构体成员名称首字母为小写时,则不进行转换。例如:weight。
  2. 如果 Tag 中定义了 json:"XXX",则 XXX 作为 JSON key。
  3. 如果没有使用 Tag json:"XXX",则 JSON key 和 Struct 成员名保持一致。
  4. 如果 Tag 中定义了 json:"-",则不进行转换。
  5. 如果 Tag 中定义了 json:",omitempty",则表示当成员数值为空时,可直接忽略。

可见,在 json 包中,Struct Tag json:"XXX" 是用来指导 json.Marshal/Unmarshal 的。

此外,我们还可以通过 Struct Tags 中的其他 Tag 来实现别的功能,例如:限定成员 Age 的值在 1-100 之间。

$ cat main.go
package main

import (
    "fmt"
    "strings"
    "strconv"
    "reflect"
  _ "encoding/json"
)

type Person struct {
    Name string    `json:"name"`
    Age  int       `json:"age" valid:"1-100"`
}

func (p * Person) validation() bool {
    v := reflect.ValueOf(*p)
    tag := v.Type().Field(1).Tag.Get("valid")
    val := v.Field(1).Interface().(int)
    fmt.Printf("tag=%v, val=%v\n", tag, val)
    
    result := strings.Split(tag, "-")
    var min, max int
    min, _ = strconv.Atoi(result[0])
    max, _ = strconv.Atoi(result[1])

    if val >= min && val <= max {
        return true
    } else {
        return false
    }
}

func main() {
    person1 := Person { "tom", 12 }
    if person1.validation() {
        fmt.Printf("person 1: valid\n")
    } else {
        fmt.Printf("person 1: invalid\n")
    }
    person2 := Person { "tom", 250 }
    if person2.validation() {
        fmt.Printf("person 2 valid\n")
    } else {
        fmt.Printf("person 2 invalid\n")
    }
}

这么例子我们给Person添加了一个validate函数,validate验证age是不是合理。这个函数可以扩展对任意struct的任意valid域进行验证。

$ cat main.go
package main

import (
    "fmt"
    "strings"
    "strconv"
    "reflect"
  _ "encoding/json"
)

type Person struct {
    Name string    `json:"name"`
    Age  int       `json:"age" valid:"1-100"`
}

type OtherStruct struct {
    Age  int       `valid:"20-300"`
}

func validateStruct(s interface{}) bool {
  v := reflect.ValueOf(s)

  for i := 0; i < v.NumField(); i++ {
    fieldTag    := v.Type().Field(i).Tag.Get("valid")
    fieldName   := v.Type().Field(i).Name
    fieldType   := v.Field(i).Type()
    fieldValue  := v.Field(i).Interface()

    if fieldTag == "" || fieldTag == "-" {
        continue
    }

    if fieldName == "Age" && fieldType.String() == "int" {
        val := fieldValue.(int)

        tmp := strings.Split(fieldTag, "-")
        var min, max int
        min, _ = strconv.Atoi(tmp[0])
        max, _ = strconv.Atoi(tmp[1])
        if val >= min && val <= max {
            return true
        } else {
            return false
        }
    }
  }
  return true
}

func main() {
    person1 := Person { "tom", 12 }
    if validateStruct(person1) {
        fmt.Printf("person 1: valid\n")
    } else {
        fmt.Printf("person 1: invalid\n")
    }

    person2 := Person { "jerry", 250 }
    if validateStruct(person2) {
        fmt.Printf("person 2: valid\n")
    } else {
        fmt.Printf("person 2: invalid\n")
    }

    other1 := OtherStruct { 12 }
    if validateStruct(other1) {
        fmt.Printf("other 1: valid\n")
    } else {
        fmt.Printf("other 1: invalid\n")
    }

    other2 := OtherStruct { 250 }
    if validateStruct(other2) {
        fmt.Printf("other 2: valid\n")
    } else {
        fmt.Printf("other 2: invalid\n")
    }
}

在这个例子中我们定义了一个函数validateStruct,接受任意一个struct作为参数;validateStruct为验证struct中所有定义的Age字段,如果字段名字是Age,字段类型是int,并且定义了valid tag,那么就会验证这个valid是否有效。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!