在咱们看一些运用反射的代码的时分,会发现,reflect.ValueOfreflect.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 吧。

那咱们什么时分应该传递指针参数呢?

什么时分传递指针?

要回答这个问题,咱们能够考虑一下以下列出的几点内容:

  1. 是否要修正变量的值,要修正就要用指针
  2. 结构体类型:是否要修正结构体里的字段,要修正就要用指针
  3. 结构体类型:是否要调用指针接纳值方法,要调用就要用指针
  4. 关于 chanmapslice 类型,咱们传递值和传递指针都能够修正其内容
  5. 关于非 interface 类型,传递给 TypeOfValueOf 的时分都会转化为 interface 类型,假如自身便是 interface 类型,则不需转化。
  6. 指针类型不行修正,可是能够修正指针指向的值。(v := reflect.ValueOf(&a)v.CanSet()falsev.Elem().CanSet()true
  7. 字符串:咱们能够对字符串进行替换,但不能修正字符串的某一个字符

大约总结下来,便是:假如咱们想修正变量的内容,就传递指针,否则就传递值。关于某些复合类型假如其内部包含了底层数据的指针, 也是能够经过传值来修正其底层数据的,这些类型有 chanmapslice。 又或许假如咱们想修正结构体类型里边的指针类型字段,传递结构体的仿制也能完成。

1. 经过传递指针修正变量的值

关于一些基础类型的变量,假如咱们想修正其内容,就要传递指针。这是由于在 go 里边参数传递都是值传递,假如咱们不传指针, 那么在函数内部拿到的仅仅参数的仿制,对其进行修正,不会影响到外部的变量(事实上在对这种反射值进行修正的时分会直接 panic)。

传值无法修正变量自身

x := 1
v := reflect.ValueOf(x)

在这个比如中,v 中保存的是 x 的仿制,对这份仿制在反射的层面上做修正其实是没有实践意义的,由于对仿制进行修正并不会影响到 x 自身。 咱们在经过反射来修正变量的时分,咱们的预期行为往往是修正变量自身。鉴于实践的运用场景,go 的反射体系现已帮咱们做了约束了, 在咱们对仿制类型的反射目标进行修正的时分,会直接 panic

深入理解 go reflect - 要不要传指针

传指针能够修正变量

x := 1
v := reflect.ValueOf(&x).Elem()

在这个比如中,咱们传递了 x 的指针到 reflect.ValueOf 中,这样一来,v 指向的便是 x 自身了。 在这种情况下,咱们对 v 的修正就会影响到 x 自身。

深入理解 go reflect - 要不要传指针

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

深入理解 go reflect - 要不要传指针

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 有两个方法 M1M2可是在实践运用中,咱们运用 p 来调用 M2 也是能够的p 之所以能调用 M2 是由于编译器帮咱们做了一些处理,将 p 转化成了 &p,然后调用 M2

深入理解 go reflect - 要不要传指针

可是在反射的时分,咱们是无法做到这一点的,这个需求特别留意。假如咱们想经过反射来调用指针接纳者的方法,就需求传递指针。

4. 变量自身包含指向数据的指针

最好不要经过值的反射目标来修正值的数据,就算有些类型能够完成这种功用。

关于 chanmapslice 这三种类型,咱们能够经过 reflect.ValueOf 来获取它们的值, 可是这个值自身包含了指向数据的指针,因而咱们仍然能够经过反射体系修正其数据。可是,咱们最好不这么用,从规范的视点,这是一种过错的操作。

经过值反射目标修正 chan、map 和 slice

深入理解 go reflect - 要不要传指针

在 go 中,chanmapslice 这几种数据结构中,存储数据都是经过一个 unsafe.Pointer 类型的变量来指向实践存储数据的内存。 这是由于,这几种类型能够存储的元素个数都是不确定的,都需求依据咱们指定的大小和存储的元素类型来进行内存分配。

正因如此,咱们仿制 chanmapslice 的时分,尽管值被仿制了一遍,可是存储数据的指针也被仿制了, 这样咱们仍然能够经过仿制的数据指针来修正其数据,如下面的比如:

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 反射目标扩容的影响

可是,咱们需求留意的是,关于 mapslice 类型,在其分配的内存容纳不下新的元素的时分,会进行扩容扩容之后,保存数据字段的指针就指向了一片新的内存了。 这意味着什么呢?这意味着,咱们经过 mapslice 的值创立的反射值目标中拿到的那份数据指针现已跟旧的 mapslice 指向的内存不相同了。

深入理解 go reflect - 要不要传指针

阐明:在上图中,咱们在反射目标中往 slice 追加元素后,导致反射目标 slicearray 指针指向了一片新的内存区域了, 这个时分咱们再对反射目标进行修正的时分,不会影响到原 slice。这也便是咱们不能经过 slicemap 的仿制的反射目标来修正 slicemap 的原因。

示例代码:

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 底层数组,可是由于在反射目标以外的 s1len 并没有产生改动, 因而 s1 仍是看不到反射目标追加的元素。所以上面说能够正常追加元素

可是,外部由于 len 没有产生改动,因而外部看不到反射目标追加的元素,所以上面也说不能正常追加元素

因而,尽管理论上修正的是同一片内存,咱们仍然不能经过传值的方式来经过反射目标往 slice 中追加元素。 可是修正 [0, len(s)) 范围内的元素在反射目标外部是能够看到的。

map 也不能经过值反射目标来修正其元素。

slice 相似,经过 map 的值反射目标来追加元素的时分,相同可能导致扩容, 扩容之后,保存数据的内存区域会产生改动。

可是,从另一个视点看,假如咱们仅仅修正其元素的话,是能够正常修正的。

chan 没有追加

chanslicemap 有个不相同的当地,它的长度是咱们创立 chan 的时分就现已固定的了, 因而,不存在扩容导致指向内存区域产生改动的问题。

因而,关于 chan 类型的元素,咱们传 ch 或许 &chreflect.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 的指针的仿制, 经过这个仿制是能够定位到 pName 字段自身指向的内存的。

可是咱们仍然是不能修正 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 的时分就不需求再做转化了。

深入理解 go reflect - 要不要传指针

interface 底层类型是指针

传递底层数据是指针类型的 interfacereflect.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 是不行寻址的。

深入理解 go reflect - 要不要传指针

上图阐明:

  • 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())
}

深入理解 go reflect - 要不要传指针

阐明:

  • 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"

这两个字符串的值是相同的,可是它们的地址是不同的。那既然如此,为什么咱们仍是不能修正它的其间某一个字符呢? 这是由于,尽管 s1s2 的地址不相同,可是它们实践保存 hello 这个字符串的地址是相同的:

v1 := (*reflect.StringHeader)(unsafe.Pointer(&s1))
v2 := (*reflect.StringHeader)(unsafe.Pointer(&s2))
// 两个字符串实例保存字符串的内存地址是相同的
assert.Equal(t, v1.Data, v2.Data)

两个字符串内存表明如下:

深入理解 go reflect - 要不要传指针

所以,咱们能够看到,s1s2 实践上是指向同一个字符串的指针,所以咱们无法修正其间某一个字符。 由于假如允许这种行为存在的话,咱们对其间一个字符串实例修正,也会影响到别的一个字符串实例。

字符串自身能够替换

尽管咱们不能修正字符串中的某一个字符,可是咱们能够经过反射目标把整个字符串替换掉:

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)
}

这能够用下图表明:

深入理解 go reflect - 要不要传指针

总结

  • 假如咱们需求经过反射目标来修正变量的值,那么咱们有必要得有方法拿到变量实践存储的内存地址。这种情况下,许多时分都是经过传递指针给 reflect.ValueOf() 方法来完成的。
  • 可是关于 chanmapslice 或许其他相似的数据结构,它们经过指针来引证实践存储数据的内存,这种数据结构是经过经过传值给 reflect.ValueOf() 方法来完成修正其间的元素的。由于这些数据结构的数据部分能够经过指针的仿制来修正。
  • 可是 mapslice 有可能会扩容,假如经过反射目标来追加元素,可能导致追加失利。这是由于,经过反射目标追加元素的时分,假如扩容了,那么本来的内存地址就会失效,这样咱们其实就修正不了本来的 mapslice 了。
  • 相同的,结构体传值来创立反射目标的时分,假如其间有指针类型的字段,那么咱们也能够经过指针来修正其间的元素。可是其他字段也仍是修正不了的。
  • 假如咱们创立反射目标的参数是 interface 类型,那么能不能修正元素的变量仍是取决于咱们这个 interface 类型变量的数据部分是值仍是指针。假如 interface 变量中存储的是值,那么咱们就不能修正其间的元素了。假如 interface 变量中存储的是指针,就能够修正。
  • 咱们无法修正字符串的某一个字符,经过反射也不能,由于字符串自身是不行变的。不同的 stirng 类型的变量,假如它们的值是相同的,那么它们会同享实践存储字符串的内存。
  • 可是咱们能够直接用一个新的字符串替代旧的字符串。

但其实说了那么多,简单来说只要一点,便是咱们只能经过反射目标来修正指针类型的变量。假如拿不到实践存储数据的指针,那么咱们就无法经过反射目标来修正其间的元素了。