在咱们看一些运用反射的代码的时分,会发现,reflect.ValueOf
或 reflect.TypeOf
的参数有些当地运用的是指针参数,有些当地又不是指针参数,
可是如同这两者在运用上没什么差异,比方下面这样:
var a = 1
v1 := reflect.ValueOf(a)
v2 := reflect.ValueOf(&a)
fmt.Println(v1.Int()) // 1
fmt.Println(v2.Elem().Int()) // 1
它们的差异貌似仅仅需不需求运用 Elem()
方法,但这个跟咱们是否传递指针给 reflect.ValueOf
其实关系不大,
相信没有人为了运用一下 Elem()
方法,就去传递指针给 reflect.ValueOf
吧。
那咱们什么时分应该传递指针参数呢?
什么时分传递指针?
要回答这个问题,咱们能够考虑一下以下列出的几点内容:
- 是否要修正变量的值,要修正就要用指针
- 结构体类型:是否要修正结构体里的字段,要修正就要用指针
- 结构体类型:是否要调用指针接纳值方法,要调用就要用指针
- 关于
chan
、map
、slice
类型,咱们传递值和传递指针都能够修正其内容 - 关于非
interface
类型,传递给TypeOf
和ValueOf
的时分都会转化为interface
类型,假如自身便是interface
类型,则不需转化。 - 指针类型不行修正,可是能够修正指针指向的值。(
v := reflect.ValueOf(&a)
,v.CanSet()
是false
,v.Elem().CanSet()
是true
) - 字符串:咱们能够对字符串进行替换,但不能修正字符串的某一个字符
大约总结下来,便是:假如咱们想修正变量的内容,就传递指针,否则就传递值。关于某些复合类型假如其内部包含了底层数据的指针,
也是能够经过传值来修正其底层数据的,这些类型有 chan
、map
、slice
。
又或许假如咱们想修正结构体类型里边的指针类型字段,传递结构体的仿制也能完成。
1. 经过传递指针修正变量的值
关于一些基础类型的变量,假如咱们想修正其内容,就要传递指针。这是由于在 go 里边参数传递都是值传递,假如咱们不传指针,
那么在函数内部拿到的仅仅参数的仿制,对其进行修正,不会影响到外部的变量(事实上在对这种反射值进行修正的时分会直接 panic
)。
传值无法修正变量自身
x := 1
v := reflect.ValueOf(x)
在这个比如中,v
中保存的是 x
的仿制,对这份仿制在反射的层面上做修正其实是没有实践意义的,由于对仿制进行修正并不会影响到 x
自身。
咱们在经过反射来修正变量的时分,咱们的预期行为往往是修正变量自身。鉴于实践的运用场景,go 的反射体系现已帮咱们做了约束了,
在咱们对仿制类型的反射目标进行修正的时分,会直接 panic
。
传指针能够修正变量
x := 1
v := reflect.ValueOf(&x).Elem()
在这个比如中,咱们传递了 x
的指针到 reflect.ValueOf
中,这样一来,v
指向的便是 x
自身了。
在这种情况下,咱们对 v
的修正就会影响到 x
自身。
2. 经过传递指针修正结构体的字段
关于结构体类型,假如咱们想修正其字段的值,也是要传递指针的。这是由于结构体类型的字段是值类型,假如咱们不传递指针,
reflect.ValueOf
拿到的也是一份仿制,对其进行修正并不会影响到结构体自身。当然,这种情况下,咱们修正它的时分也会 panic
。
type person struct {
Name string
Age int
}
p := person{
Name: "foo",
Age: 30,
}
// v 本质上是指向 p 的指针
v := reflect.ValueOf(&p)
// v.CanSet() 为 false,v 是指针,指针自身是不能修正的
// v.Elem() 是 p 自身,是能够修正的
fmt.Println(v.Elem().FieldByName("Name").CanSet()) // true
fmt.Println(v.Elem().FieldByName("Age").CanSet()) // true
3. 结构体:获取指针接纳值方法
关于结构体而言,假如咱们想经过反射来调用指针接纳者方法,那么咱们需求传递指针。
在开端讲解这一点之前,需求就以下内容达到一致:
type person struct {
}
func (p person) M1() {
}
func (p *person) M2() {
}
func TestPerson(t *testing.T) {
p := person{}
v1 := reflect.ValueOf(p)
v2 := reflect.ValueOf(&p)
assert.Equal(t, 1, v1.NumMethod())
assert.Equal(t, 2, v2.NumMethod())
// v1 和 v2 都有 M1 方法
assert.True(t, v1.MethodByName("M1").IsValid())
assert.True(t, v2.MethodByName("M1").IsValid())
// v1 没有 M2 方法
// v2 有 M2 方法
assert.False(t, v1.MethodByName("M2").IsValid())
assert.True(t, v2.MethodByName("M2").IsValid())
}
在上面的代码中,p
只要一个方法 M1
,而 &p
有两个方法 M1
和 M2
。
可是在实践运用中,咱们运用 p 来调用 M2 也是能够的,
p
之所以能调用 M2
是由于编译器帮咱们做了一些处理,将 p
转化成了 &p
,然后调用 M2
。
可是在反射的时分,咱们是无法做到这一点的,这个需求特别留意。假如咱们想经过反射来调用指针接纳者的方法,就需求传递指针。
4. 变量自身包含指向数据的指针
最好不要经过值的反射目标来修正值的数据,就算有些类型能够完成这种功用。
关于 chan
、map
、slice
这三种类型,咱们能够经过 reflect.ValueOf
来获取它们的值,
可是这个值自身包含了指向数据的指针,因而咱们仍然能够经过反射体系修正其数据。可是,咱们最好不这么用,从规范的视点,这是一种过错的操作。
经过值反射目标修正 chan、map 和 slice
在 go 中,chan
、map
、slice
这几种数据结构中,存储数据都是经过一个 unsafe.Pointer
类型的变量来指向实践存储数据的内存。
这是由于,这几种类型能够存储的元素个数都是不确定的,都需求依据咱们指定的大小和存储的元素类型来进行内存分配。
正因如此,咱们仿制 chan
、map
、slice
的时分,尽管值被仿制了一遍,可是存储数据的指针也被仿制了,
这样咱们仍然能够经过仿制的数据指针来修正其数据,如下面的比如:
func TestPointer1(t *testing.T) {
// 数组需求传递引证才干修正其元素
arr := [3]int{1, 2, 3}
v1 := reflect.ValueOf(&arr)
v1.Elem().Index(1).SetInt(100)
assert.Equal(t, 100, arr[1])
// chan 传值也能够修正其元素
ch := make(chan int, 1)
v2 := reflect.ValueOf(ch)
v2.Send(reflect.ValueOf(10))
assert.Equal(t, 10, <-ch)
// map 传值也能够修正其元素
m := make(map[int]int)
v3 := reflect.ValueOf(m)
v3.SetMapIndex(reflect.ValueOf(1), reflect.ValueOf(10))
assert.Equal(t, 10, m[1])
// slice 传值也能够修正其元素
s := []int{1, 2, 3}
v4 := reflect.ValueOf(s)
v4.Index(1).SetInt(20)
assert.Equal(t, 20, s[1])
}
slice 反射目标扩容的影响
可是,咱们需求留意的是,关于 map
和 slice
类型,在其分配的内存容纳不下新的元素的时分,会进行扩容,
扩容之后,保存数据字段的指针就指向了一片新的内存了。
这意味着什么呢?这意味着,咱们经过 map
和 slice
的值创立的反射值目标中拿到的那份数据指针现已跟旧的 map
和 slice
指向的内存不相同了。
阐明:在上图中,咱们在反射目标中往 slice
追加元素后,导致反射目标 slice
的 array
指针指向了一片新的内存区域了,
这个时分咱们再对反射目标进行修正的时分,不会影响到原 slice
。这也便是咱们不能经过 slice
或 map
的仿制的反射目标来修正 slice
或 map
的原因。
示例代码:
func TestPointer1(t *testing.T) {
s := []int{1, 2, 3}
v4 := reflect.ValueOf(s)
v4.Index(1).SetInt(20)
assert.Equal(t, 20, s[1])
// 这儿产生了扩容
// v5 的 array 跟 s 的 array 指向的是不同的内存区域了。
v5 := reflect.Append(v4, reflect.ValueOf(4))
fmt.Println(s) // [1 20 3]
fmt.Println(v5.Interface().([]int)) // [1 20 3 4]
// 这儿修正 v5 的时分影响不到 s 了
v5.Index(1).SetInt(30)
fmt.Println(s) // [1 20 3]
fmt.Println(v5.Interface().([]int)) // [1 30 3 4]
}
阐明:在上面的代码中,v5
实践上是 v4
扩容后的切片,底层的 array
指针指向的是跟 s
不相同的 array
了,
因而在咱们修正 v5
的时分,会发现本来的 s
并没有产生改动。
尽管经过值反射目标能够修正 slice 的数据,可是假如经过反射目标 append 元素到 slice 的反射目标的时分, 可能会触发 slice 扩容,这个时分再修正反射目标的时分,就影响不了本来的 slice 了。
slice 容量够的话是不是就能够正常追加元素了?
只能说,能,也不能。咱们看看下面这个比如:
func TestPointer000(t *testing.T) {
s1 := make([]int, 3, 6)
s1[0] = 1
s1[1] = 2
s1[2] = 3
fmt.Println(s1) // [1 2 3]
v6 := reflect.ValueOf(s1)
v7 := reflect.Append(v6, reflect.ValueOf(4))
// 尽管 s1 的容量足够大,可是 s1 仍是看不到追加的元素
fmt.Println(s1) // [1 2 3]
fmt.Println(v7.Interface().([]int)) // [1 2 3 4]
// s1 和 s2 底层数组仍是同一个
// array1 是 s1 底层数组的内存地址
array1 := (*(*reflect.SliceHeader)(unsafe.Pointer(&s1))).Data
s2 := v7.Interface().([]int)
// array2 是 s2 底层数组的内存地址
array2 := (*(*reflect.SliceHeader)(unsafe.Pointer(&s2))).Data
assert.Equal(t, array1, array2)
// 这是由于 s1 的长度并没有产生改动,
// 所以 s1 看不到追加的那个元素
fmt.Println(len(s1), cap(s1)) // 3 6
fmt.Println(len(s2), cap(s2)) // 4 6
}
在这个比如中,咱们给 slice
分配了足够大的容量,可是咱们经过反射目标来追加元素的时分,
尽管数据被正常追加到了 s1
底层数组,可是由于在反射目标以外的 s1
的 len
并没有产生改动,
因而 s1
仍是看不到反射目标追加的元素。所以上面说能够正常追加元素。
可是,外部由于 len
没有产生改动,因而外部看不到反射目标追加的元素,所以上面也说不能正常追加元素。
因而,尽管理论上修正的是同一片内存,咱们仍然不能经过传值的方式来经过反射目标往 slice
中追加元素。
可是修正 [0, len(s))
范围内的元素在反射目标外部是能够看到的。
map 也不能经过值反射目标来修正其元素。
跟 slice
相似,经过 map
的值反射目标来追加元素的时分,相同可能导致扩容,
扩容之后,保存数据的内存区域会产生改动。
可是,从另一个视点看,假如咱们仅仅修正其元素的话,是能够正常修正的。
chan 没有追加
chan
跟 slice
、map
有个不相同的当地,它的长度是咱们创立 chan
的时分就现已固定的了,
因而,不存在扩容导致指向内存区域产生改动的问题。
因而,关于 chan
类型的元素,咱们传 ch
或许 &ch
给 reflect.ValueOf
都能够完成修正 ch
。
结构体字段包含指针的情况
假如结构体里边包含了指针字段,咱们也仅仅想经过反射目标来修正这个指针字段的话,
那么咱们也仍是能够经过传值给 reflect.ValueOf
来创立反射目标来修正这个指针字段:
type person struct {
Name *string
}
func TestPointerPerson(t *testing.T) {
name := "foo"
p := person{Name: &name}
v := reflect.ValueOf(p)
fmt.Println(v.Field(0).Elem().CanAddr())
fmt.Println(v.Field(0).Elem().CanSet())
name1 := "bar"
v.Field(0).Elem().Set(reflect.ValueOf(name1))
// p 的 Name 字段现已被成功修正
fmt.Println(*p.Name)
}
在这个比如中,咱们尽管运用了 p
而不是 &p
来创立反射目标,
可是咱们仍然能够修正 Name
字段,由于反射目标拿到了 Name
的指针的仿制,
经过这个仿制是能够定位到 p
的 Name
字段自身指向的内存的。
可是咱们仍然是不能修正 p
中的其他字段。
5. interface 类型处理
关于 interface
类型的元素,咱们能够将以下两种操作看作是等价的:
// v1 跟 v2 都拿到了 a 的仿制
var a = 1
v1 := reflect.ValueOf(a)
var b interface{} = a
v2 := reflect.ValueOf(b)
咱们能够经过下面的断言来证明:
assert.Equal(t, v1.Kind(), v2.Kind())
assert.Equal(t, v1.CanAddr(), v2.CanAddr())
assert.Equal(t, v1.CanSet(), v2.CanSet())
assert.Equal(t, v1.Interface(), v2.Interface())
当然,关于指针类型也是相同的:
// v1 跟 v2 都拿到了 a 的指针
var a = 1
v1 := reflect.ValueOf(&a)
var b interface{} = &a
v2 := reflect.ValueOf(b)
相同的,咱们能够经过下面的断言来证明:
assert.Equal(t, v1.Kind(), v2.Kind())
assert.Equal(t, v1.Elem().Kind(), v2.Elem().Kind())
assert.Equal(t, v1.Elem().CanAddr(), v2.Elem().CanAddr())
assert.Equal(t, v1.Elem().Addr(), v2.Elem().Addr())
assert.Equal(t, v1.Interface(), v2.Interface())
assert.Equal(t, v1.Elem().Interface(), v2.Elem().Interface())
interface 底层类型是值
interface
类型的底层类型是值的时分,咱们将其传给 reflect.ValueOf
跟直接传值是相同的。
是没有方法修正 interface
底层数据的值的(除了指针类型字段,由于反射目标也拿到了指针字段的地址):
type person struct {
Name *string
}
func TestInterface1(t *testing.T) {
name := "foo"
p := person{Name: &name}
// v 拿到的是 p 的仿制
// 下面两行等价于 v := reflect.ValueOf(p)
var i interface{} = p
v := reflect.ValueOf(i)
assert.False(t, v.CanAddr())
assert.Equal(t, reflect.Struct, v.Kind())
assert.True(t, v.Field(0).Elem().CanAddr())
}
在上面这个比如中 v := reflect.ValueOf(i)
其实等价于 v := reflect.ValueOf(p)
,
由于在咱们调用 reflect.ValueOf(p)
的时分,go 言语自身会帮咱们将 p
转化为 interface{}
类型。
在咱们赋值给 i
的时分,go 言语也会帮咱们将 p
转化为 interface{}
类型。
这样再调用 reflect.ValueOf
的时分就不需求再做转化了。
interface 底层类型是指针
传递底层数据是指针类型的 interface
给 reflect.ValueOf
的时分,咱们能够修正 interface
底层指针指向的值,
效果等同于直接传递指针给 reflect.ValueOf
:
func TestInterface(t *testing.T) {
var a = 1
v1 := reflect.ValueOf(&a)
var b interface{} = &a
v2 := reflect.ValueOf(b)
// v1 和 v2 本质上都接纳了一个 interface 参数,
// 这个 interface 参数的数据部分都是 &a
v1.Elem().SetInt(10)
assert.Equal(t, 10, a)
// 经过 v1 修正 a 的值,v2 也能看到
assert.Equal(t, 10, v2.Elem().Interface())
// 相同的,经过 v2 修正 a 的值,v1 也能看到
v2.Elem().SetInt(20)
assert.Equal(t, 20, a)
assert.Equal(t, 20, v1.Elem().Interface())
}
不要再对接口类型取地址
能不能经过反射 Value 目标来修正变量只取决于,能不能依据反射目标拿到开始变量的内存地址。 假如拿到的仅仅原始值的仿制,不管咱们怎么做都无法修正原始值。
关于初学者别的一个令人困惑的当地可能是下面这样的代码:
func TestInterface(t *testing.T) {
var a = 1
var i interface{} = a
v1 := reflect.ValueOf(&a)
v2 := reflect.ValueOf(&i)
// v1 和 v2 的类型都是 reflect.Ptr
assert.Equal(t, reflect.Ptr, v1.Kind())
assert.Equal(t, reflect.Ptr, v2.Kind())
// 可是两者的 Elem() 类型不同,
// v1 的 Elem() 是 reflect.Int,
// v2 的 Elem() 是 reflect.Interface
assert.Equal(t, reflect.Int, v1.Elem().Kind())
assert.Equal(t, reflect.Interface, v2.Elem().Kind())
}
困惑的源头在于,reflect.ValueOf()
这个函数的参数是 interface{}
类型的,
这意味着咱们能够传递任意类型的值给它,包含指针类型的值。
正因如此,假如咱们不懂得 reflect
包的工作原理的话,
就会传错变量到 reflect.ValueOf()
函数中,导致程序犯错。
关于上面比如的 v2
,它是一个指向 interface{}
类型的指针的反射目标,它也能找到开始的变量 a
:
可是能不能修正
a
,仍是取决于a
是否是可寻址的。也便是开始传递给i
的值是不是一个指针类型。
assert.Equal(t, "<*interface {} Value>", v2.String())
assert.Equal(t, "<interface {} Value>", v2.Elem().String())
assert.Equal(t, "<int Value>", v2.Elem().Elem().String())
在上面的比如中,咱们传递给 i
的是 a
的值,而不是 a
的指针,所以 i
是不行寻址的,也便是说 v2
是不行寻址的。
上图阐明:
-
i
是接口类型,它的数据部分是a
的仿制,它的类型部分是int
类型。 -
&i
是指向接口的指针,它指向了上图的eface
。 -
v2
是指向eface
的指针的反射目标。 - 最终,咱们经过
v2
找到i
这个接口,然后经过i
找到a
这个变量的仿制。
所以,绕了一大圈,咱们最终仍是修正不了 a
的值。到最后咱们只拿到了 a
的仿制。
6. 指针类型反射目标不行修正其指向地址
其实这一点上面有些当地也有涉及到,可是这儿再着重一下。一个比如如下:
func TestPointer(t *testing.T) {
var a = 1
var b = &a
v := reflect.ValueOf(b)
assert.False(t, v.CanAddr())
assert.False(t, v.CanSet())
assert.True(t, v.Elem().CanAddr())
assert.True(t, v.Elem().CanSet())
}
阐明:
-
v
是指向&a
的指针的反射目标。 - 经过这个反射目标的
Elem()
方法,咱们能够找到原始的变量a
。 - 反射目标自身不能修正,可是它的
Elem()
方法返回的反射目标能够修正。
关于指针类型的反射目标,其自身不能修正,可是它的
Elem()
方法返回的反射目标能够修正。
7. 反射也不能修正字符串中的字符
这是由于,go 中的字符串自身是不行变的,咱们无法像在 C 言语中那样修正其间某一个字符。
其实不止是 go,其实许多编程言语的字符串都是不行变的,比方 Java 中的 String
类型。
在 go 中,字符串是用一个结构体来表明的,大约长下面这个样子:
type StringHeader struct {
Data uintptr
Len int
}
-
Data
是指向字符串的指针。 -
Len
是字符串的长度(单位为字节)。
在 go 中 str[1] = 'a'
这样的操作是不允许的,由于字符串是不行变的。
相同的字符串只要一个实例
假设咱们定义了两个相同的字符串,如下:
s1 := "hello"
s2 := "hello"
这两个字符串的值是相同的,可是它们的地址是不同的。那既然如此,为什么咱们仍是不能修正它的其间某一个字符呢?
这是由于,尽管 s1
和 s2
的地址不相同,可是它们实践保存 hello
这个字符串的地址是相同的:
v1 := (*reflect.StringHeader)(unsafe.Pointer(&s1))
v2 := (*reflect.StringHeader)(unsafe.Pointer(&s2))
// 两个字符串实例保存字符串的内存地址是相同的
assert.Equal(t, v1.Data, v2.Data)
两个字符串内存表明如下:
所以,咱们能够看到,s1
和 s2
实践上是指向同一个字符串的指针,所以咱们无法修正其间某一个字符。
由于假如允许这种行为存在的话,咱们对其间一个字符串实例修正,也会影响到别的一个字符串实例。
字符串自身能够替换
尽管咱们不能修正字符串中的某一个字符,可是咱们能够经过反射目标把整个字符串替换掉:
func TestStirng(t *testing.T) {
s := "hello"
v := reflect.ValueOf(&s)
fmt.Println(v.Elem().CanAddr())
fmt.Println(v.Elem().CanSet())
v.Elem().SetString("world")
fmt.Println(s) // world
}
这儿实践上是把 s
中保存字符串的地址替换成了指向 world
这个字符串的地址,而不是将 hello
指向的内存修正成 world
:
func TestStirng(t *testing.T) {
s := "hello"
oldAddr := (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
v := reflect.ValueOf(&s)
v.Elem().SetString("world")
newAddr := (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
// 修正之后,实践保存字符串的内存地址产生了改动
assert.NotEqual(t, oldAddr, newAddr)
}
这能够用下图表明:
总结
- 假如咱们需求经过反射目标来修正变量的值,那么咱们有必要得有方法拿到变量实践存储的内存地址。这种情况下,许多时分都是经过传递指针给
reflect.ValueOf()
方法来完成的。 - 可是关于
chan
、map
和slice
或许其他相似的数据结构,它们经过指针来引证实践存储数据的内存,这种数据结构是经过经过传值给reflect.ValueOf()
方法来完成修正其间的元素的。由于这些数据结构的数据部分能够经过指针的仿制来修正。 - 可是
map
和slice
有可能会扩容,假如经过反射目标来追加元素,可能导致追加失利。这是由于,经过反射目标追加元素的时分,假如扩容了,那么本来的内存地址就会失效,这样咱们其实就修正不了本来的map
和slice
了。 - 相同的,结构体传值来创立反射目标的时分,假如其间有指针类型的字段,那么咱们也能够经过指针来修正其间的元素。可是其他字段也仍是修正不了的。
- 假如咱们创立反射目标的参数是
interface
类型,那么能不能修正元素的变量仍是取决于咱们这个interface
类型变量的数据部分是值仍是指针。假如interface
变量中存储的是值,那么咱们就不能修正其间的元素了。假如interface
变量中存储的是指针,就能够修正。 - 咱们无法修正字符串的某一个字符,经过反射也不能,由于字符串自身是不行变的。不同的
stirng
类型的变量,假如它们的值是相同的,那么它们会同享实践存储字符串的内存。 - 可是咱们能够直接用一个新的字符串替代旧的字符串。
但其实说了那么多,简单来说只要一点,便是咱们只能经过反射目标来修正指针类型的变量。假如拿不到实践存储数据的指针,那么咱们就无法经过反射目标来修正其间的元素了。