日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

go?reflect要不要傳指針原理詳解_Golang

作者:eleven26 ? 更新時間: 2023-02-07 編程語言

正文

在我們看一些使用反射的代碼的時候,會發現,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 吧。

那我們什么時候應該傳遞指針參數呢?

什么時候傳遞指針?

要回答這個問題,我們可以思考一下以下列出的幾點內容:

  • 是否要修改變量的值,要修改就要用指針
  • 結構體類型:是否要修改結構體里的字段,要修改就要用指針
  • 結構體類型:是否要調用指針接收值方法,要調用就要用指針
  • 對于 chanmapslice 類型,我們傳遞值和傳遞指針都可以修改其內容
  • 對于非 interface 類型,傳遞給 TypeOfValueOf 的時候都會轉換為 interface 類型,如果本身就是 interface 類型,則不需轉換。
  • 指針類型不可修改,但是可以修改指針指向的值。(v := reflect.ValueOf(&a)v.CanSet()falsev.Elem().CanSet()true
  • 字符串:我們可以對字符串進行替換,但不能修改字符串的某一個字符

大概總結下來,就是:如果我們想修改變量的內容,就傳遞指針,否則就傳遞值。對于某些復合類型如果其內部包含了底層數據的指針, 也是可以通過傳值來修改其底層數據的,這些類型有 chanmapslice。 又或者如果我們想修改結構體類型里面的指針類型字段,傳遞結構體的拷貝也能實現。

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 有兩個方法 M1M2但是在實際使用中,我們使用 p 來調用 M2 也是可以的p 之所以能調用 M2 是因為編譯器幫我們做了一些處理,將 p 轉換成了 &p,然后調用 M2

但是在反射的時候,我們是無法做到這一點的,這個需要特別注意。如果我們想通過反射來調用指針接收者的方法,就需要傳遞指針。

4. 變量本身包含指向數據的指針

最好不要通過值的反射對象來修改值的數據,就算有些類型可以實現這種功能。

對于 chanmapslice 這三種類型,我們可以通過 reflect.ValueOf 來獲取它們的值, 但是這個值本身包含了指向數據的指針,因此我們依然可以通過反射系統修改其數據。但是,我們最好不這么用,從規范的角度,這是一種錯誤的操作。

通過值反射對象修改 chan、map 和 slice

在 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 指向的內存不一樣了。

說明:在上圖中,我們在反射對象中往 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 的時候就不需要再做轉換了。

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 是不可尋址的。

上圖說明:

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

這兩個字符串的值是相同的,但是它們的地址是不同的。那既然如此,為什么我們還是不能修改它的其中某一個字符呢? 這是因為,雖然 s1s2 的地址不一樣,但是它們實際保存 hello 這個字符串的地址是一樣的:

v1 := (*reflect.StringHeader)(unsafe.Pointer(&s1))
v2 := (*reflect.StringHeader)(unsafe.Pointer(&s2))
// 兩個字符串實例保存字符串的內存地址是一樣的
assert.Equal(t, v1.Data, v2.Data)

兩個字符串內存表示如下:

所以,我們可以看到,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)
}

這可以用下圖表示:

總結

  • 如果我們需要通過反射對象來修改變量的值,那么我們必須得有辦法拿到變量實際存儲的內存地址。這種情況下,很多時候都是通過傳遞指針給 reflect.ValueOf() 方法來實現的。
  • 但是對于 chanmapslice 或者其他類似的數據結構,它們通過指針來引用實際存儲數據的內存,這種數據結構是通過通過傳值給 reflect.ValueOf() 方法來實現修改其中的元素的。因為這些數據結構的數據部分可以通過指針的拷貝來修改。
  • 但是 mapslice 有可能會擴容,如果通過反射對象來追加元素,可能導致追加失敗。這是因為,通過反射對象追加元素的時候,如果擴容了,那么原來的內存地址就會失效,這樣我們其實就修改不了原來的 mapslice 了。
  • 同樣的,結構體傳值來創建反射對象的時候,如果其中有指針類型的字段,那么我們也可以通過指針來修改其中的元素。但是其他字段也還是修改不了的。
  • 如果我們創建反射對象的參數是 interface 類型,那么能不能修改元素的變量還是取決于我們這個 interface 類型變量的數據部分是值還是指針。如果 interface 變量中存儲的是值,那么我們就不能修改其中的元素了。如果 interface 變量中存儲的是指針,就可以修改。
  • 我們無法修改字符串的某一個字符,通過反射也不能,因為字符串本身是不可變的。不同的 stirng 類型的變量,如果它們的值是一樣的,那么它們會共享實際存儲字符串的內存。
  • 但是我們可以直接用一個新的字符串替代舊的字符串。

但其實說了那么多,簡單來說只有一點,就是我們只能通過反射對象來修改指針類型的變量。如果拿不到實際存儲數據的指針,那么我們就無法通過反射對象來修改其中的元素了。

原文鏈接:https://juejin.cn/post/7183998435245162552

欄目分類
最近更新