Go语言中反射简介

参考:
http://c.biancheng.net/view/4407.html

关键点

通过关键词汇,实现快速理解,记忆目的
类型Type与种类Kind的区别

1、Go语言程序中的类型(Type)指的是?

  • A、系统原生数据类型
  • int,

  • string

  • bool

  • float32等等

  • B、以及使用 type 关键字定义类型
    这些类型名称就是其类型本身名称
    例如使用 type A struct{} 定义结构体时,A就是 struct{} 的类型。

2、种类(Kind)指的是对象归属的品种

获取种类Kind

1、reflect 包

Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 TypeValue 任意接口值在反射中都可以理解为由 reflect.Typereflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 ValueType。

2、反射的类型对象(reflect.Type)

2.1、如何获取任意值的类型对象reflect.Type

在Go语言程序中,使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),

2.2、类型对象有什么用?

程序通过类型对象可以访问任意值的类型信息,

2.3、示例 获取类型对象

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var a int
    typeOfA := reflect.TypeOf(a)
    fmt.Println(typeOfA.Name(), typeOfA.Kind())
}

运行结果如下:

int  int

代码说明如下:

  • 第 9 行,定义一个 int 类型的变量。

  • 第 10 行,通过 reflect.TypeOf() 取得变量 a 的类型对象 typeOfA,类型为 reflect.Type()。

  • 第 11 行中,通过 typeOfA 类型对象的成员函数,可以分别获取到 typeOfA 变量的类型名为 int,种类(Kind)为 int。

reflect.TypeOf来获取类型对象Type

3、反射的类型(Type)与种类(Kind)

在使用反射时,需要首先理解类型(Type)种类(Kind)的区别。

编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。

例如需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。

1) 反射种类(Kind)的定义

类型Type与种类Kind的区别

1、Go语言程序中的类型(Type)指的是?

  • A、系统原生数据类型
  • int,

  • string

  • bool

  • float32等等

  • B、以及使用 type 关键字定义类型
    这些类型名称就是其类型本身名称
    例如使用 type A struct{} 定义结构体时,A就是 struct{} 的类型。

2、种类(Kind)指的是对象归属的品种

在 reflect 包中有如下定义:

type Kind uint
const (
    Invalid Kind = iota  // 非法类型
    Bool                 // 布尔型
    Int                  // 有符号整型
    Int8                 // 有符号8位整型
    Int16                // 有符号16位整型
    Int32                // 有符号32位整型
    Int64                // 有符号64位整型
    Uint                 // 无符号整型
    Uint8                // 无符号8位整型
    Uint16               // 无符号16位整型
    Uint32               // 无符号32位整型
    Uint64               // 无符号64位整型
    Uintptr              // 指针
    Float32              // 单精度浮点数
    Float64              // 双精度浮点数
    Complex64            // 64位复数类型
    Complex128           // 128位复数类型
    Array                // 数组
    Chan                 // 通道
    Func                 // 函数
    Interface            // 接口
    Map                  // 映射
    Ptr                  // 指针
    Slice                // 切片
    String               // 字符串
    Struct               // 结构体
    UnsafePointer        // 底层指针
)

Map、Slice、Chan 属于引用类型,使用起来类似指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。

type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。

2) 从类型对象中获取类型名称和种类

A、通过反射如何获取类型名称

Go语言中的类型名称对应的反射获取方法是 reflect.Type 中的 Name() 方法,返回表示类型名称字符串

B、如何获取种类

类型归属的种类(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 类型的常量。

下面的代码中会对常量和结构体进行类型信息获取。

package main
import (
    "fmt"
    "reflect"
)
// 定义一个Enum类型
type Enum int
const (
    Zero Enum = 0
)
func main() {
    // 声明一个空结构体
    type cat struct {
    }
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(cat{})
    // 显示反射类型对象的名称和种类
    fmt.Println(typeOfCat.Name(), typeOfCat.Kind())
    // 获取Zero常量的反射类型对象
    typeOfA := reflect.TypeOf(Zero)
    // 显示反射类型对象的名称和种类
    fmt.Println(typeOfA.Name(), typeOfA.Kind())
}

运行结果如下:

cat struct
Enum int

代码说明如下:

  • 第 17 行,声明结构体类型 cat。
  • 第 20 行,将 cat 实例化,并且使用 reflect.TypeOf() 获取被实例化后的 cat 的反射类型对象。
  • 第 22 行,输出 cat 的类型名称和种类,类型名称就是 cat,而 cat 属于一种结构体种类,因此种类为 struct。
  • 第 24 行,Zero 是一个 Enum 类型的常量。这个 Enum 类型在第 9 行声明,第 12 行声明了常量。如没有常量也不能创建实例,通过 reflect.TypeOf() 直接获取反射类型对象。
获取种类Kind
  • 第 26 行,输出 Zero 对应的类型对象的类型名和种类。

4、指针与指针指向元素

4.1、如何通过反射来获取指针指向的元素类型种类

Go语言程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型,这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作,代码如下:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // 声明一个空结构体
    type cat struct {
    }
    // 创建cat的实例
    ins := &cat{}
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(ins)
    // 显示反射类型对象的名称和种类
    fmt.Printf("name:'%v' kind:'%v'\n", typeOfCat.Name(), typeOfCat.Kind())
    // 取类型的元素
    typeOfCat = typeOfCat.Elem()
    // 显示反射类型对象的名称和种类
    fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
}

运行结果如下:

name:'' kind:'ptr'
element name: 'cat', element kind: 'struct'

代码说明如下:

  • 第 13 行,创建了 cat 结构体的实例,ins 是一个 *cat 类型的指针变量

  • 第 15 行,对指针变量获取反射类型信息。

  • 第 17 行,输出指针变量的类型名称和种类。Go语言的反射中对所有指针变量的种类都是Ptr,但需要注意的是,指针变量的类型名称是,不是 *cat

  • 第 19 行,取指针类型的元素类型,也就是 cat 类型。这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。

  • 第 21 行,输出指针变量指向元素的类型名称和种类,得到了 cat 的类型名称(cat)和种类(struct)。

获取指针指向元素的类型对象从而获取类型的名称和种类

5、使用反射获取结构体的成员类型

任意值通过 reflect.TypeOf() 获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象 reflect.Type 的 NumField() 和 Field() 方法获得结构体成员的详细信息。

与成员获取相关的 reflect.Type 的方法如下表所示。


结构体成员访问的方法列表

1) 结构体字段类型

reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(StructTag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。

StructField 的结构如下:

type StructField struct {
    Name string          // 字段名
    PkgPath string       // 字段路径
    Type      Type       // 字段反射类型对象
    Tag       StructTag  // 字段的结构体标签
    Offset    uintptr    // 字段在结构体中的相对偏移
    Index     []int      // Type.FieldByIndex中的返回的索引值
    Anonymous bool       // 是否为匿名字段
}

字段说明如下:

  • Name:为字段名称。

  • PkgPath:字段在结构体中的路径。

  • Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。

  • Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。

  • Index:FieldByIndex 中的索引顺序。

  • Anonymous:表示该字段是否为匿名字段。

2) 获取成员反射信息

下面代码中,实例化一个结构体并遍历其结构体成员,再通过 reflect.Type 的 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。

反射访问结构体成员类型及信息:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // 声明一个空结构体
    type cat struct {
        Name string
        // 带有结构体tag的字段
        Type int `json:"type" id:"100"`
    }
    // 创建cat的实例
    ins := cat{Name: "mimi", Type: 1}
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(ins)
    // 遍历结构体所有成员
    for i := 0; i < typeOfCat.NumField(); i++ {
        // 获取每个成员的结构体字段类型
        fieldType := typeOfCat.Field(i)
        // 输出成员名和tag
        fmt.Printf("name: %v  tag: '%v'\n", fieldType.Name, fieldType.Tag)
    }
    // 通过字段名, 找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        // 从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

代码输出如下:

name: Name  tag: ''
name: Type  tag: 'json:"type" id:"100"'
type 100

代码说明如下:

  • 第 10 行,声明了带有两个成员的 cat 结构体。

  • 第 13 行,Type 是 cat 的一个成员,这个成员类型后面带有一个以 ` 开始和结尾的字符串。这个字符串在Go语言中被称为 Tag(标签)。一般用于给字段添加自定义信息,方便其他模块根据信息进行不同功能的处理。

  • 第 16 行,创建 cat 实例,并对两个字段赋值。结构体标签属于类型信息,无须且不能赋值。

  • 第 18 行,获取实例的反射类型对象。

  • 第 20 行,使用 reflect.Type 类型的 NumField() 方法获得一个结构体类型共有多少个字段。如果类型不是结构体,将会触发宕机错误。

  • 第 22 行,reflect.Type 中的 Field() 方法和 NumField 一般都是配对使用,用来实现结构体成员的遍历操作。

  • 第 24 行,使用 reflect.Type 的 Field() 方法返回的结构不再是 reflect.Type 而是 StructField 结构体。

  • 第 27 行,使用 reflect.Type 的 FieldByName() 根据字段名查找结构体字段信息,catType 表示返回的结构体字段信息,类型为 StructField,ok 表示是否找到结构体字段的信息。

  • 第 29 行中,使用 StructField 中 Tag 的 Get() 方法,根据 Tag 中的名字进行信息获取。

6、结构体标签(Struct Tag)

通过 reflect.Type 获取结构体成员信息 reflect.StructField 结构中的 Tag 被称为结构体标签(StructTag)

结构体标签是对结构体字段的额外信息标签

JSON、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。

这些信息都是静态的,无须实例化结构体,可以通过反射获取到。

1) 结构体标签的格式

Tag 在结构体字段后方书写的格式如下:

`key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成;键与值使用冒号分隔,值用双引号括起来;键值对之间使用一个空格分隔。

2) 从结构体标签中获取值

StructTag 拥有一些方法,可以进行 Tag 信息的解析和提取,如下所示:

  • func (tag StructTag) Get(key string) string:根据 Tag 中的键获取对应的值,例如key1:"value1" key2:"value2"的 Tag 中,可以传入“key1”获得“value1”。

  • func (tag StructTag) Lookup(key string) (value string, ok bool):根据 Tag 中的键,查询值是否存在。

3) 结构体标签格式错误导致的问题

编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,示例代码如下:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type cat struct {
        Name string
        Type int `json: "type" id:"100"`
    }
    typeOfCat := reflect.TypeOf(cat{})
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        fmt.Println(catType.Tag.Get("json"))
    }
}

运行上面的代码会输出一个空字符串,并不会输出期望的 type。

代码第 11 行中,在 json: 和 "type" 之间增加了一个空格,这种写法没有遵守结构体标签的规则,因此无法通过 Tag.Get 获取到正确的 json 对应的值。这个错误在开发中非常容易被疏忽,造成难以察觉的错误。所以将第 12 行代码修改为下面的样子,则可以正常打印。

type cat struct {
    Name string
    Type int `json:"type" id:"100"`
}

运行结果如下:

type
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • 第一次知道反射的时候还是许多年前在学校里玩 C# 的时候。那时总是弄不清楚这个复杂的玩意能有什么实际用途……然后发...
    勿以浮沙筑高台阅读 1,126评论 0 9
  • 欢迎访问博客原文:http://pengtuo.tech/golang/2019/09/23/golang-ref...
    PeTu阅读 625评论 0 0
  • 那日,风轻日暖,于骊山之下,枫叶长者之家见人写春联,谈笑间,龙飞凤舞,妙笔生花,不觉技痒。东施效颦,博大家一乐。 ...
    W和W阅读 401评论 1 3
  • 最近是真的真的很烦躁。虽然公司好心地预留了时间让员工有机会在正式离职前找下家,但是宁愿不知道,自从知道了以后,一直...
    柳树发芽阅读 132评论 0 0
  • 那些忘不掉的过往,躲在你的意识里,一旦触发犹如快窒息的瞬间却获得了氧气 。 是新生,却也是毁灭。 1.那个被选择忘...
    瑛姐Zoe阅读 703评论 3 10