如何理解 Go 中的反射
首先给大家推荐一个在线 Golang 运行环境,可以测试简短的代码逻辑。
Golang 中的反射是基于类型(type
)机制的,所以需要重温一下 Golang
中的类型机制。
1. Types and interfaces
Go 是静态类型语言。 每个变量都有一个静态类型,也就是在编译时已知并固定的一种类型:int,float32,*MyType,[]byte
等。 如果我们声明:
type MyInt int
var i int
var j MyInt
则变量 i
是 int
类型,变量 j
是 MyInt
类型。变量 i
和 j
具有不同的静态类型,尽管它们具有相同的基础类型,但是如果不进行转换依然无法将其中一个变量赋值于另一个变量。
Go 中一个重要的类别是接口类型(interface),接口表示固定的方法集。接口变量可以存储任何具体的(非接口)值,只要该值实现了接口中所有定义的方法即可。 一个重要的例子就是io.Reader
与io.Writer
, 类型 Reader
与 Writer
都来自 io - The Go Programming Language 包
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
任何只要实现了 Read
或者 Write
方法的类型都算作实现了 io.Reader
或者 io.Writer
接口,这意味着 io.Reader
类型的变量可以保存其类型具有 Read
方法的任何值:
var r io.Reade
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
重要的是要清楚,无论 r
可能包含什么具体值,r
的类型始终是 io.Reader
:Go是静态类型的,而 r
的静态类型是io.Reader
。
接口类型的一个非常重要的例子是空接口:
interface{}
它表示空方法集,并且任何值都满足实现了空接口,因为任何值具有零个或多个方法,而空接口没有方法供实现。
有人说 Go
的空接口是动态类型的,但这会产生误导。它们是静态类型的:接口类型的变量始终具有相同的静态类型,即使在运行时存储在接口变量中的值可能会更改类型,但该值也还是始终满足接口的要求。
2. The representation of an interface
接口类型的变量存储一对儿信息,分别是分配给该变量的具体值以及该值的类型描述符。 例如:
var r io.Reade
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, er
}
r = tty
在变量 r
中则存储了 (value, type)
对,内容为 (tty, *os.File)
。值得注意的是,即使接口变量 r
仅提供对 Read
方法的访问,但内部的值仍包含有关该值的所有类型信息。所以下面这个代码也是正确的:
var w io.Write
w = r.(io.Writer)
这个赋值操作中的表达式是类型断言。它断言 r
内的项也实现了 io.Writer
,因此我们可以将其分配给接口变量 w
。赋值后,w
也同样包含一对信息 —— (tty,* os.File)
。接口的静态类型会决定使用接口变量调用哪些方法,即使内部的具体值可能具有更大的方法集。
强调一遍,在一个接口变量中一直都是保存一对信息,格式为 (value, concrete type)
,但是不能保存 (value, interface type)
格式。 > 在 Go 语言中,变量类型分为两大类,concrete type
与 interface type
{ > concrete type: 指具体的变量类型,可以是基本类型,也可以是自定义类型或者结构体类型; > interface type: 指接口类型,可以是 Golang 内置的接口类型,或者是使用者自定义的接口类型;}
而之所以先重温接口就是因为反射和接口息息相关
3. Three law of reflection
3.1. Reflection goes from interface value to reflection object.
从底层层面来说,反射是一种解释存储在接口类型变量中的 (type, value)
一对信息的机制。首先,我们需要在反射包中了解两种类型:type
和 value
,通过这两种类型对接口变量内容的访问,还有两个对应的函数,称为 reflect.TypeOf
和reflect.ValueOf
,从接口值中获取 reflect.Type
和 reflect.Value
部分。 例如 TypeOf
:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
fmt.Println("value:", reflect.ValueOf(x))
}
结果输出为:
type: float64
value: 3.4
说明:
- reflect.TypeOf:获得值的类型(
type
),如float64、int、pointer、struct
等等真实的类型; - reflect.ValueOf:获得值的内容,如1.2345这个具体数值,或者类似
&{1 “Allen.Wu” 25}
这样的结构体 struct 的内容; - 说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是
reflect.Type
和reflect.Value
这两个函数的返回;
reflect.TypeOf
的函数签名包括一个空接口:
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
当我们调用 reflect.TypeOf(x)
时,x
首先存储在一个空接口中,然后将其作为参数传递; reflect.TypeOf
解压缩该空接口以恢复类型信息。 又例如:
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
fmt.Println("value:", reflect.ValueOf(x).String())
输出结果为:
value: 3.4
value: <float64 Value>
reflect.Type
和 reflect.Value
都有很多方法可以让我们检查和操作它们。 一个重要的例子是 Value
具有 Type
方法,该方法返回 reflect.Value
的 Type
。另一个是 Type
和 Value
都有 Kind
方法,该方法返回一个常量,指示存储的项目类型:Uint,Float64,Slice
等。
反射库具有几个值得一提的属性。
首先,为使 API 保持简单,Value
的 “getter”
和 “setter”
方法在可以容纳该值的最大类型上运行:例如,所有有符号整数的 int64
。也就是说,Value
的 Int
方法返回一个 int64
,而 SetInt
值采用一个 int64
,可能需要转换为涉及的实际类型:
var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())
第二个属性是反射对象的 Kind()
方法描述基础类型,而不是静态类型。例如:
package main
import (
"fmt"
"reflect"
)
func main() {
type MyInt int // 反射对象包含用户定义的整数类型的值
var x MyInt = 7
v := reflect.TypeOf(x)
fmt.Println(v)
fmt.Println(v.Kind())
}
则会输出:
main.MyInt
int
3.2. Reflection goes from reflection object to interface value.
Golang 的反射也有其逆向过程。
给定一个 reflect.Value
,我们可以使用 Interface()
方法恢复接口值,该方法将 type
和 value
信息打包回接口表示形式并返回结果:
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
例如:
func main() {
var xx float64 = 3.4
v := reflect.ValueOf(xx) // v is a reflection object
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
fmt.Printf("%T", y)
}
输出结果为:
3.4
float64
简而言之,Interface
方法与 ValueOf
函数相反,但其结果始终是静态类型 interface{}
。
所以综上述两点可得知,Golang 中的反射可理解为包含两个过程,一个是接口值到反射对象的过程,另一个则是反向的反射对象到接口值的过程。
3.3. To modify a reflection object, the value must be settable.
第三条规律则是如果想要修改一个反射对象(reflection object
),那么这个对象的值必须是可设置的。直接这样说会比较困惑,从例子出发:
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
如果运行上述这个代码,则会报错提示:
panic: reflect: reflect.Value.SetFloat using unaddressable value
在这个例子中,反射对象 v 的值就是不可设置的,执行下述代码:
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
则会显示:
settability of v: false
那么什么是可设置的呢,在 Golang 官网原文有这么一句
Settability is determined by whether the reflection object holds the original item.
翻译过来就是可设置性由反射对象是否保留原始对象确定。我们都知道在 Go 中的参数传递都是使用的值传递的方法,即将原有值的拷贝传递,在刚刚的例子中,我们是传递了一个 x
对象的拷贝到 reflect.ValueOf
函数中,而不是 x
对象本身,刚刚的 SetFloat
将更新存储在反射对象内的 x
的副本,并且 x
本身将不受影响,在 Go 中这是不合理的,可设置性就是避免此问题的属性。
而如果我们想要修改其内容,很简单,将对象的指针传入其中,于是刚刚的代码可以改为:
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
fmt.Println("----------------")
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(z)
此时输出:
float64type of p: *float64
settability of p: false
settability of v: true
----------------
7.1
7.1
3.4 Reflection and Structs
反射修改内容一个经常使用的地方就是通过指针修改传入的结构体的字段值,只要我们能够获得该结构体对象的指针。
一个简单的示例。
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %vn", i, typeOfT.Field(i).Name, f.Type(), f.Interface())
}
这里使用结构的地址创建了反射对象,然后稍后将要对其进行修改。将 typeOfT
设置为其类型,并使用简单的方法调用对字段进行迭代。请注意,我们从结构类型中提取了字段的名称,但是字段本身是常规的 reflect.Value
对象。这里结果输出为:
0: A int = 23
1: B string = skidoo
这里有一点要注意的是,结构体 T 的字段名首字母都是大写,在 Go 中首字母大写的变量或者函数才是可导出的(exported),相当于 Java 中的 public,而首字母小写的变量或者函数则是包外不可使用,对应 Java 的 protected。 而只有可导出的结构体字段此方式才能修改。
现在我们可以试着修改结构体 T
:
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
// output is "t is now {77 Sunset Strip}"
4. Conclusion
反射的三条规律:
- 反射包括从接口值到反射对象的过程;
- 反射也包括从反射对象到接口值的过程;
- 要修改反射对象,该值必须可设置(
To modify a reflection object, the value must be settable.
)。
参考文献
- 【Java学习笔记之二十四】对Java多态性的一点理解
- 【Java学习笔记之二十六】深入理解Java匿名内部类
- 【Java学习笔记之二十五】初步认知Java内部类
- AIM Tech Round 4 (Div. 2)(A,暴力,B,组合数,C,STL+排序)
- 【Java学习笔记之三十】详解Java单例(Singleton)模式
- 基于Windows下处理Java错误:编码GBK的不可映射字符的解决方案
- 浅析ASCII、Unicode和UTF-8三种常见字符编码
- 【Java学习笔记之三十一】详解Java8 lambda表达式
- 2017 Multi-University Training Contest - Team 9 1003&&HDU 6163 CSGO【计算几何】
- 【Code】关关的刷题日记21——Leetcode 485. Max Consecutive Ones
- 2017 Multi-University Training Contest - Team 9 1002&&HDU 6162 Ch’s gift【树链部分+线段树】
- 【Java学习笔记之三十二】浅谈Java中throw与throws的用法及异常抛出处理机制剖析
- Linux上访问SQL Server数据库
- 2017 Multi-University Training Contest - Team 9 1001&&HDU 6161 Big binary tree【树形dp+hash】
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 前端进阶知识汇总
- 前端也能学算法:由浅入深讲解动态规划
- 轻松理解JS中的面向对象,顺便搞懂prototype和__proto__
- 前端也能学算法:由浅入深讲解贪心算法
- web.py指南性说明
- this到底指向啥?看完这篇就知道了!
- 学以致用:手把手教你撸一个工具库并打包发布,顺便解决JS小数计算不准问题
- python 实现 php 的 var_dump 功能
- RSA初探,聊聊怎么破解HTTPS
- 深入解析Underscore.js源码架构
- python正向连接后门
- setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop
- emlog全版本CSRF加用户xsser.me模块
- 从发布订阅模式入手读懂Node.js的EventEmitter源码
- 手写一个Promise/A+,完美通过官方872个测试用例