之前在博客中有讲Reflect
的使用, 然而使用后仍然对于Reflect
存在疑惑, 知其然不知其所以然. 所以本文讲如何理解Reflect
在Go
中的工作原理;
前提
这一部分讲述, 理解Reflect
所需要的前置知识;
众所周知Reflect
提供了程序运行时检查数据结构, 修改数据内容的能力.
在Go
中, 其依赖两个Feature in Go
: Type
, Interface
.
Type
Go
是**静态类型(static type
)**语言, 其类型在编译时就已经固定了. 例如:
1 | package main |
输出为:
1 | Variable i type info is: int |
可以发现, 即使i
, j
的底层类型(underlying type
)一致, 对于Go
来说其static type
是不一致的;
Interface
Interface Type
在Type
中存在一个特殊的类型Interface
, 其表示某特定方法集.只要某个具体变量concrete variable
满足Interface
声明的方法集, 那么就可以通过Interface variable
接收此concrete variable
. 例如io.Reader, io.Writer
:
1 | type Reader interface { |
只要实现了Read/Write
方法, 就可以使用interface variable
接收:
1 | var r io.Reader |
上面的例子中, 将*os.File
类型,其实现了Read
,的变量tty
赋值给了interface variable: r
;
Empty Interface Type
在Interface
中也有一个特殊的类型: 空接口Empty Interface: interface {}
, 别名为any
. 我们都知道, Empty interface
中不包含任何方法, 任意的类型, 都实现了Empty Interface
, 也就是说:
Empty Interface Variable
可以接收任意Variable
;
1 | var i any |
上面通过Empty Interface Variable: i
来接收tty
当然是没问题的;
Interface Variable Representation
而对于Interface Variable
其表现形式为: 值与类型对 (Value, Concrete Type) pair
: Interface Variable
存储了一个具体的值, 与这个具体的值的类型;
- 具体的值: 指接口内接收的值;
- 具体的类型: 指的是这个值所代表的类型, 由于
Go
是静态类型语言, 这个类型中不仅仅包含有满足当前Interface Type
的内容, 也包含有其他类型自定义的类型; 简单说: 这个类型, 就是值的类型, 其具备这个类型中定义的所有内容.下面的例子将解释:
1 | package main |
输出为:
1 | Variable r type info is: *os.File |
我们发现, 可以通过断言(assert
)的方式, 将Reader Interface Variable
赋值给Writer Interface Variable
. 这是因为Reader Interface Variable: r
的Concrete type: *os.File
也实现了Write
方法. 也就是说通过Interface Variable
接收Concrete Variable
并不会丢失任何concrete type
的信息, 其仅仅会约束Interface Variable
的调用: 例如r
仅可以调用Read
方法.
对于empty interface
来说, 则可以直接赋值, 不需要assert
: 因为empty interface
是所有类型的子集:
1 | ... |
三个反射定律
From Interface value
To Reflect Object
Go
中的reflect pkg
核心Object
为: reflect.Type, reflect.Value
; 这两种类型, 赋予了编程人员获取Interface Value
中(Value, Concrete Type) pair
的能力.
获取:
reflect.Type
的方式:reflect.TypeOf
1
2
3
4
5
6
7
8
9
10
11package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}输出为:
type: float64
;上面的例子中, 通过
reflect.TypeOf
方法获取了x
的类型信息, 并输出.
然而我们要阐述的是Interace value -> reflect Object
的转换, 不是concrete value -> reflect Object
对于Go
有所了解的朋友肯定意识到,reflect.TypeOf(x)
一定是做了隐式转换, 将concrete value
先转换为了interface value
, 随后在对interface value
进行了提取其type
信息的操作;
这可以通过reflect.TypeOf
的函数签名印证此推论:1
2
3
4
5// 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 any) Type {
...
}可以看到
reflect.TypeOf
的入参类型为empty interface: any
. 对于Go
的函数调用来说, 其均为值传递, 会复制一份数据, 并赋值给函数入参, 所以上面的示例可以通过伪代码描述为:1
2var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(i = copy(x)))获取:
reflect.Type
的方式:reflect.ValueOf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("reflect value:", reflect.ValueOf(x))
fmt.Println("value string:", reflect.ValueOf(x).String())
var y string = "SUN"
fmt.Println("reflect value:", reflect.ValueOf(y))
fmt.Println("value string:", reflect.ValueOf(y).String())
}输出为:
1
2
3
4reflect value: 3.4
value string: <float64 Value>
reflect value: SUN
value string: SUN上面的例子中通过
reflect.ValueOf
方法, 将interface value
转换为reflect.Value
;reflect.ValueOf
的函数签名入参也是empty interface
类型, 其参数调用的过程与reflect.TypeOf
是一致的, 这里不赘述;需要注意的是:
reflect.ValueOf
获得的是reflect.Value
对象,reflect.VauleOf.String
获得的是string
对象. 如果对于interface value
其可以转换为string
的话, 则输出value
本身, 例如"SUN"
; 如果不可以转换的话, 则输出格式是固定的<T Value>
, 例如3.4
;
reflect
也提供了reflect.Value, reflect.Type
的操作方法:
reflect.Value.Type
: 将reflect.Value -> reflect.Type
;reflect.Value.Kind
,reflect.Type.Kind
: 获取reflct.Object
的underlying type
;- …
由于并非本文重点, 这里不一一解释;
From Reflect Object
To Interface Value
既然上面可以将Interface value
转换为reflect Object
, 那么这个过程也可以反转, 其方法为: reflect.Object.Interface
; 这里以reflect.Value
为例:
1 | package main |
输出为: Interface value: 3.4
.
在上面的例子中通过Interface()
方法, 将reflect.Value
再次封装为(Value, Concrete Type) pair
形式的Interface Value
, 其类型与值信息并未改变;
如果underlying type
类型一致的情况下, 是否会发生类型变动呢? 答案是否定的:
1 | package main |
输出为:
1 | Interface value: 3.4 |
在上面的例子中, 我们定义了一个自定义类型 MyFloat
,并创建了一个变量 x
,类型为 MyFloat
,值为 3.4
。我们通过reflect.Interface
方法, 获得了一个Interface Value: i
, 并再次检查其类型, 发现其与最初的变量x
的static type
一致: MyFloat
, 当然啦Underlying type
是float64
; 上面的例子说明了Interface value -> reflcet Object -> Interface value
的过程是幂等的, 对于Interface Value
的值与类型两种信息, 均没有丢失;
Modify a Reflect Object
利用reflect
可以修改reflect Object
的前提是settable
;何为settable
?
错误示例:
1
2
3
4
5
6
7
8
9
10
11package main
import (
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
}输出为:
1
2
3
4
5panic: reflect: reflect.Value.SetFloat using unaddressable value
goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x7f2889534108?)
....上面是示例中, 运行报错; 其提示为: 使用了无法引用的值; 其实这里的报错是不精准的, 其核心是
v
是unsettable
状态, 而不是unaddressable
;我们可以通过
CanSet
方法, 判定是否可修改:1
2
3
4
5
6
7
8
9
10
11
12package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
}输出为:
settability of v: false
正确示例:
我们需要使用
reflect.ValueOf(&x).Elem()
来获取x
的可设置反射值,然后才能使用v.SetFloat(7.1)
来修改变量x
的值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(&x).Elem()
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(7.1)
fmt.Println("修改后的 x 的值:", x)
}输出为:
settability of v: true <br> 修改后的 x 的值: 7.1
;
何为settable
? 其与addressable
很像但不一样, 其表示是否可以通过v
找到原始变量的值, 或者说是 reflect.Object
是否存储了原始的值;
为何对于reflect.ValueOf(x)
就不可以, 对于reflect.ValueOf(&x).Elem
就可以呢? 这是由于Go
中函数传参是值传递的特性导致的;reflect.ValueOf(x) == reflect.Vaule(i = copy(x))
.那么传值本身是绝对无法获取到原始值的, 传指针则有可能获得指针指向的值本身.为了更清晰的表示, 这里再写一个例子:
1 | package main |
输出为:
1 | type of v: *float64 |
上面的例子中将reflect.ValueOf.Elem
过程拆分为两步: reflect.ValueOf, reflect.ValueOf.Elem
, 并分别检验其settable
. 发现只要是直接传参的都不可以更改, 只有这种reflect.ValueOf(&x).Elem
形式才是找到了原始值存储的地址;
对于struct
的修改也是异曲同工, 网上例子很多, 这里不赘述了;
结论
本文介绍了reflect
的三定律:
- From
Interface value
To `Reflect Object - From
Reflect Object
ToInterface Value
- Modify a
Reflect Object
一旦理解了上述过程, 对于实际使用时可以减少不必要的困惑. 当然了, 还是那个建议, 能不用反射就别用
;