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. K8s 里如何优雅地使用 /dev/shm 实现容器间共享内存

    目录 1. 从 docker run 的 --shm-size 参数聊起 2. Linux 里的 /dev/shm 3. Docker 对共享内存的支持 4. K8s 里如何设置 /dev/shm 大 ...

  2. A Novel Cascade Binary Tagging Framework for Relational Triple Extraction(论文研读与复现)

    A Novel Cascade Binary Tagging Framework for Relational Triple Extraction Zhepei Wei,Jianlin Su, Yue ...

  3. python 获取apk 信息

    python 获取apk 信息 1.安装androguard pip install androguard 2.获取apk的相关信息 引入基础信息 from androguard.misc impor ...

  4. ubuntu安装mysql8,debian安装mysql8,linux安装mysql8,x86_64架构,deb包

    作者主页:https://www.cnblogs.com/milkbox 参考: 修改大小写:MySQL8.0安装后更改不区分大小写!包你必生效!_mysql8.0不区分大小写-CSDN博客 整个安装 ...

  5. Git commit emoji 对照表

    emoji emoji代码 commit说明 (调色板) :art: 改进代码结构/代码格式 ️ (闪电) :zap: 提升性能 (赛马) :racehorse: 提升性能 (火焰) :fire: 移 ...

  6. 如何在IIS上部署docsify以及404问题

    操作步骤 创建一个文件夹,在文件夹中新建2个文件 index.html:入口文件,整个网站只需要这个html文件,其他文件都是md文件 README.md:主页内容,如果没有这个文件,访问时提示404 ...

  7. 将一个服务器通过HTTP请求另一个服务器

    1.一个服务器访问另一个服务器(不传参) @RequestMapping ("/test") public ResponseEntity<String> serverT ...

  8. maven系列:聚合与继承

    目录 一.聚合 创建Maven模块,设置打包类型为pom 设置当前聚合工程所包含的子模块名称 二. 继承 问题导入 创建Maven模块,设置打包类型为pom 在父工程的pom文件中配置依赖关系(子工程 ...

  9. 有了这个算法,图像上文字擦除再也用不上PS了

    摘要:本文介绍几篇关于自然场景下文字擦除的论文工作. 图像文字擦除方法 给定一幅自然场景图像,只将图像中文字区域抹去而不改动其他区域像素值的方法称为文字擦除算法.该方法在隐私保护,身份信息篡改,数据增 ...

  10. 释放千行百业数据价值,华为云DAYU有一套

    摘要:结合数字化转型中行业面临的挑战及产品解决方案解读数据使能服务DAYU. 大禹(DAYU)治水是一个有美好寓意的故事,大禹汲取了父亲治水的经验教训,总结出一套行之有效的治水方法,对洪水进行治理疏导 ...