1. 反射简介

反射是 元编程 概念下的一种形式,它在运行时操作不同类型的对象,检查对象的类型,大小等信息,对于没有源代码的包反射尤其有用。

设想一个场景,读取一个包中变量 a 的类型,并打印该类型的信息。可以通过 type/switch 判断如下:

switch t := a.(type) {
case int:
fmt.Println(t)
case float64:
fmt.Printf(t)
...
}

很快发现一个问题,通过 type switch 判段变量类型会有问题:

  1. 由于不知道变量的类型,所以 case 里需要写很多类型以获得匹配项。
  2. 如果类型是自定义结构体,case 无法提前预知该结构体,从而匹配不到自定义结构体。

这是 Go 的语法元素较少,设计简单导致没有特别强的表达能力,而通过反射 reflect 可以弥补,增强操作类型对象的表达能力。

2. 反射三大法则

反射中最重要的函数莫过于 reflect.TypeOf 和 reflect.ValueOf,它们对应的类型分别是 reflect.Type 和 reflect.Value:

// reflect.TypeOf, reflect.Type
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
} // reflect.ValueOf, reflect.Value
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
} escapes(i) return unpackEface(i)
}

reflect.TypeOf 和 reflect.ValueOf 函数的作用是什么,这在标准库里就有描述,直接摘录如下:

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...} // ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {...}

通过一段代码示例进一步了解 TypeOf 和 ValueOf:

p := person{"chunqiu", 1}

v := reflect.ValueOf(p)
fmt.Println(v) t := reflect.TypeOf(p)
fmt.Println(t) // result:
{chunqiu 1}
main.person

可以看到 TypeOf 返回反射对象的类型,ValueOf 返回反射对象的值。

为什么涉及到反射对象呢?这里实际上做了两次“转换”,第一次传入 reflect.TypeOf 和 reflect.ValueOf 时从具体类型转换为 interface{} 变量,接着从 interface{} 变量反射出反射对象。在反射机制下,逆着转换也是可以的,也就是从反射对象转换为 interface{} 变量。

进一步的介绍反射的三大法则,后续介绍皆围绕着三大法则进行:

  1. 从 interface{} 变量可以反射出反射对象。
  2. 从反射对象可以反射到 interface{} 变量。
  3. 修改反射对象,其值必须可设置。

2.1 反射法则一

从 interface{} 变量可以反射出反射对象。上例中代码即是这一情况。以 TypeOf 举例:

func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}

函数传参将外部传入的结构体类型变量转换为空接口类型变量 i,通过 unsafe.Pointer 指针转换指针到 emptyInterface 指针,最后解引用得到空接口类型的变量 eface,返回变量的类型 eface.typ。

这一过程需要介绍的有 unsafe.Pointer 指针和 emptyInterface 结构体。

2.1.1 unsafe.Pointer

顾名思义 unsafe.Pointer 指针是个不安全的指针,类似于 C 中的 void * 。它可以指向任何类型的变量,并且与 uintptr 结合可对内存数据进行直接运算,当然这种操作是很危险,也是官方不推荐的。

除了直接操作内存数据外,unsafe.Pointer 还可以强制转换类型的指针到特定类型的指针,如:

var i int = 1

var pi *int = &i
fmt.Printf("&pi: %v, pi: %v, *pi: %v\n", &pi, pi, *pi)
var pf *float64 = (*float64)(unsafe.Pointer(pi))
fmt.Printf("&pf: %v, pf: %v, *pf: %v\n", &pf, pf, *pf) *pf = *pf * 10
fmt.Printf("f: %d\n", i) // result
&pi: 0xc000006028, pi: 0xc000014088, *pi: 1
&pf: 0xc000006038, pf: 0xc000014088, *pf: 5e-324
f: 10

unsafe.Pointer 指针接受的是指针变量 pi 的值,也就是整型变量 i 的地址值。该指针变量(pi) 通过 unsafe.Pointer 这一座桥被转换为 *float64 类型的指针变量 pf,此时同 pi 一样,pf 也指向了变量 i 的地址值。修改 *pf 等于修改变量 i 的值。

在这里有两点要注意的是:

  1. 变量 pi 实际可以省略,可直接向 unsafe.Pointer 传变量 i 的地址 &i。
  2. 不能像 unsafe.Pointer 传递值,编译器会报错。

2.1.2 emptyInterface

emptyInterface 是 interface{} 类型的实际结构体表示,可在源码中查看 emptyInterface 的定义:

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}

其中,rtype 用于表示变量的类型,word 是个 unsafe.Pointer 指针,它指向内部封装的数据。

再回到 eface := *(*emptyInterface)(unsafe.Pointer(&i)) 这里。interface{} 变量被转换成内部的 emptyInterface 表示,然后从中获取相应的类型信息。

前面介绍了 TypeOf 函数获得类型信息的实现,进一步看 ValueOf 函数实现:

func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
} escapes(i) return unpackEface(i)
}

ValueOf 首先调用 escapes 保证当前值逃逸到堆上,然后在 unpackEface 函数调用 *(*emptyInterface)(unsafe.Pointer(&i)) 将空接口变量转换成内部的 emptyInterface 表示,再获取相应的类型信息。

对于 Printf 等函数打印也是用到了反射机制来识别传入变量的参数,类型等信息的。比如:

v := reflect.ValueOf(p)
fmt.Println(v) // result:
{chunqiu 1}

v 是返回的 Value 类型的结构体,其结构体表示为:

type Value struct {
typ *rtype ptr unsafe.Pointer flag
}

通过 Println 打印的是反射对象的值。而不是 Value 的值表示。举个例子,如下结构体打印:

type ValueTest struct {
typ *int
word *int
flag bool
} var x int = 1
vtest := ValueTest{&x, &x, true}
fmt.Println(vtest) // result
{0xc000014088 0xc000014088 true}

针对不同类型结构体 Println 打印不同信息。

2.1.3 逃逸分析

针对上例说的 escapes 设置变量逃逸到堆上,有必要展开说明。详细了解可看 Go 逃逸分析

这里对变量逃逸做一个总结,并对其中栈空间不足这种逃逸情况做个实践。

变量逃逸的四大情况:

  1. 指针逃逸;
  2. interface{} 动态类型逃逸。(前面的 escapes 即是设置 interface{} 变量逃逸到堆上),为什么这么设置看 escapes 函数注释即可明白:
     // TODO: Maybe allow contents of a Value to live on the stack.
    // For now we make the contents always escape to the heap. It
    // makes life easier in a few places (see chanrecv/mapassign
    // comment below).
    escapes(i)
  3. 栈空间不足;
  4. 闭包;

2.1.4 栈空间不足

查看机器上栈允许占用的内存大小:

$ ulimit -a
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
...

验证是否超过一定大小的局部变量将逃逸到堆上:

func generate8191() {
nums := make([]int, 8191) // < 64KB
for i := 0; i < 8191; i++ { nums[i] = 0 }
} func generate8192() {
nums := make([]int, 8192) // = 64KB
for i := 0; i < 8192; i++ { nums[i] = 0 } } func generate(n int) {
nums := make([]int, n) // 不确定大小
for i := 0; i < n; i++ { nums[i] = 0 }
} func main() {
generate8191()
generate8192()
generate(1)
}

编译上述代码:

# go build -gcflags=-m main_stack.go
# command-line-arguments
./main_stack.go:4:6: can inline generate8191
./main_stack.go:9:6: can inline generate8192
./main_stack.go:15:6: can inline generate
./main_stack.go:20:6: can inline main
./main_stack.go:21:17: inlining call to generate8191
./main_stack.go:22:17: inlining call to generate8192
./main_stack.go:23:13: inlining call to generate
./main_stack.go:5:14: make([]int, 8191) does not escape
./main_stack.go:10:14: make([]int, 8192) escapes to heap
./main_stack.go:16:14: make([]int, n) escapes to heap
./main_stack.go:21:17: make([]int, 8191) does not escape
./main_stack.go:22:17: make([]int, 8192) escapes to heap
./main_stack.go:23:13: make([]int, n) escapes to heap

可以看到当切片内存超过或达到栈内存限制大小时将逃逸到堆上,对于不确定切片长度的对象也将逃逸到堆上。具体分析可见文章 Go 内存逃逸

2.2 反射法则二

反射的第二法则是从反射对象反射回接口 interface{} 变量。反射回 interface{} 变量可进一步还原成变量最原始的状态。当然,对于原始状态是 interface{} 类型的,就不需要反射成变量最原始状态这一步了。

首先,从反射对象反射回接口 interface{} 变量:

p := person{"chunqiu", 27, true, false}

pi := reflect.ValueOf(p).Interface()

fmt.Printf("pi interface: %v, pi type: %T\n", pi, pi)

if v, ok := pi.(person); ok {
fmt.Printf("pi is the value of interface{}: %v\n", v)
} // result
pi interface: {chunqiu 27 true false}, pi type: main.person
pi is the value of interface{}: {chunqiu 27 true false}

分开看:

reflect.ValueOf(p) 将原始状态转换为 interface{} 类型值,接着反射为反射对象。

reflect.ValueOf(p).Interface{} 将反射对象转换为 Interface{} 类型的值。

接着将 interface{} 类型的值转换为原始状态 person 结构体的值:

fmt.Printf("person struct: %v, person type: %T\n", reflect.ValueOf(p).Interface().(person), reflect.ValueOf(p).Interface().(person))

// result
person struct: {chunqiu 27 true false}, person type: main.person

虽然打印结果一样,但实际上 reflect.ValueOf(p).Interface().(person) 已经是原始状态 person 结构体的值而不是接口 interface{} 的值了,可以通过接口断言来验证这一点:

/*
if v, ok := reflect.ValueOf(p).Interface().(person).(p); ok {
fmt.Printf("reflect.ValueOf(p).Interface().(person) is a interface: %v\n", v)
}
*/ // invalid type assertion: reflect.ValueOf(p).Interface().(person).(p) (non-interface type person on left)

实际上法则二是法则一的逆过程,为什么可以这样可以通过 Interface() 函数继续分析,这里不详细展开了:

// Interface returns v's current value as an interface{}.
// It is equivalent to:
// var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
return valueInterface(v, true)
}

2.3 反射法则三

法则三是修改反射对象,其值必须可设置。举例如下:

type MyInt int
var m MyInt = 5 reflect.ValueOf(m).SetInt(43)
fmt.Println(m) // result
panic: reflect: reflect.Value.SetInt using unaddressable value

运行出错,提示 using unaddressable value。这是因为传入 ValueOf 的是值,值会在赋值给函数参数的时候进行拷贝,所以在 ValueOf 内做的操作和传入的变量没有关系。需要传地址给 ValueOf:

reflect.ValueOf(&m).Elem().SetInt(43)
fmt.Println(m) // result
43

与前面不同的是这里使用了 Elem 函数,Elem 函数会获取指针指向的变量,在调用 SetInt 更新变量的值。看起来复杂,其过程等价于:

m := (*int)(unsafe.Pointer(i))
*m = 43

3. 反射对象方法

通过 TypeOf 和 ValueOf 反射的对象可调用相应的反射对象方法获得反射对象信息。

方法很多,可从源码中查看所需的方法。这里主要介绍 Value 的 Kind 方法,Kind 方法在源码中大量使用,比如自定义结构体,对于源码包来说并不关心它的类型是哪种自定义类型,而是关心传入值是结构体 struct, 接口 interface,指针还是其它类型。通过 Kind 方法即可以查找相关变量的底层类型。

如 type 自定义 int 类型:

type MyInt int
var m MyInt = 5 t := reflect.ValueOf(m).Kind()
fmt.Println(t) // result
int

Kind 返回 m 的底层类型 int。同理,对于 struct 和指针也可使用 Kind 获得变量的底层类型:

var age int = 1
var p *int = &age
fmt.Println(reflect.ValueOf(p).Kind()) n := name{age: 1}
fmt.Println(reflect.ValueOf(n).Kind()) // result
ptr
struct

小白学标准库之反射 reflect的更多相关文章

  1. go标准库的学习-reflect

    参考: https://studygolang.com/pkgdoc http://c.biancheng.net/golang/concurrent/ 导入方式: import "refl ...

  2. 小白学 Python 爬虫(21):解析库 Beautiful Soup(上)

    小白学 Python 爬虫(21):解析库 Beautiful Soup(上) 人生苦短,我用 Python 前文传送门: 小白学 Python 爬虫(1):开篇 小白学 Python 爬虫(2):前 ...

  3. 小白学 Python 爬虫(22):解析库 Beautiful Soup(下)

    人生苦短,我用 Python 前文传送门: 小白学 Python 爬虫(1):开篇 小白学 Python 爬虫(2):前置准备(一)基本类库的安装 小白学 Python 爬虫(3):前置准备(二)Li ...

  4. 小白学 Python 爬虫(23):解析库 pyquery 入门

    人生苦短,我用 Python 前文传送门: 小白学 Python 爬虫(1):开篇 小白学 Python 爬虫(2):前置准备(一)基本类库的安装 小白学 Python 爬虫(3):前置准备(二)Li ...

  5. 小白学 Python 爬虫(32):异步请求库 AIOHTTP 基础入门

    人生苦短,我用 Python 前文传送门: 小白学 Python 爬虫(1):开篇 小白学 Python 爬虫(2):前置准备(一)基本类库的安装 小白学 Python 爬虫(3):前置准备(二)Li ...

  6. 什么是 C 和 C ++ 标准库?学编程的你应该知道这些知识!

    简要介绍编写C/C ++应用程序的领域,标准库的作用以及它是如何在各种操作系统中实现的. 我已经接触C++一段时间了,一开始就让我感到疑惑的是其内部结构:我所使用的内核函数和类从何而来? 谁发明了它们 ...

  7. 【循序渐进学Python】11.常用标准库

    安装完Python之后,我们也同时获得了强大的Python标准库,通过使用这些标准库可以为我们节省大量的时间.这里是一些常用标准库的简单说明.更多的标准库的说明,可以参考Python文档 sys 模块 ...

  8. C++著名类库和C++标准库介绍

    C++著名类库 1.C++各大有名库的介绍——C++标准库 2.C++各大有名库的介绍——准标准库Boost 3.C++各大有名库的介绍——GUI 4.C++各大有名库的介绍——网络通信 5.C++各 ...

  9. Go语言反射reflect

    目录 通过反射获取类型信息 理解反射的类型(Type)与种类(Kind) reflect.Elem() - 通过反射获取指针指向的元素类型 通过反射获取结构体的成员类型 通过反射获取值信息 使用反射值 ...

  10. go学习笔记-标准库

    标准库 名称 摘要 archive tar tar包实现了tar格式压缩文件的存取. zip zip包提供了zip档案文件的读写服务. bufio bufio 包实现了带缓存的I/O操作. built ...

随机推荐

  1. Kernel Memory 入门系列:文档的管理

    Kernel Memory 入门系列: 文档的管理 在Quick Start中我们了解到如何快速直接地上传文档.当时实际中,往往会面临更多的问题,例如文档如何更新,如何划定查询范围等等.这里我们将详细 ...

  2. C++ Qt开发:SqlRelationalTable关联表组件

    Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍SqlRela ...

  3. Windows手工入侵排查思路

    文章来源公众号:Bypass Windows系统被入侵后,通常会导致系统资源占用过高.异常端口和进程.可疑的账号或文件等,给业务系统带来不稳定等诸多问题.一些病毒木马会随着计算机启动而启动并获取一定的 ...

  4. JavaFx 打包jar(六)

    JavaFx 打包jar(六) JavaFX 从入门入门到入土系列 我们编写了不少javafx,那么如何打包成jar给用户呢?下面我给出比较全的打包方式. 打包jar 下面我给出比较全的打包方式. 1 ...

  5. javacv实现直播流

    javacv实现直播流 javacv从入门到入土系列,音视频入门有一点门槛的延迟大概是2~4秒之间, 依赖 <!-- 需要注意,javacv主要是一组API为主,还需要加入对应的实现 --> ...

  6. struts2 Filter中无法转发请求

    struts2 Filter中无法转发请求 项目升级struts2版本为最新以修复漏洞,由于一些历史原因,部分访问在升级后访问404,直接对历史代码改造代价太大. 于是使用拦截器对其转发.重定向,但是 ...

  7. Ubuntu 终端如何分割多个窗口

    sudo apt-get install terminator 查看 ~/.config(隐藏文件夹 ctrl + h 即可看见) 下是否有 terminator 文件夹 如果没有手动创建一个 然后在 ...

  8. 如何从零开始实现TDOA技术的 UWB 精确定位系统(4)

    这是一个系列文章<如何从零开始实现TDOA技术的 UWB 精确定位系统>第4部分. 重要提示(劝退说明): Q:做这个定位系统需要基础么?A:文章不是写给小白看的,需要有电子技术和软件编程 ...

  9. 第三部分_Shell脚本简单四则运算

    简单四则运算 算术运算:默认情况下,shell就只能支持简单的整数运算 运算内容:加(+).减(-).乘(*).除(/).求余数(%) 1. 四则运算符号 表达式 举例 $(( )) | echo $ ...

  10. 【昇腾】ModelArts与Atlas 200 DK云端协同开发——行人检测Demo(完整版)

    摘要:本文主要为大家展示如何基于ModelArts与Atlas 200 DK 端云协同开发的行人检测Demo实现过程. 基于开源数据集,使用ModelArts训练行人检测模型,在本地MindStudi ...