網站首頁 編程語言 正文
正文
在我們看一些使用反射的代碼的時候,會發現,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
類型的變量,如果它們的值是一樣的,那么它們會共享實際存儲字符串的內存。 - 但是我們可以直接用一個新的字符串替代舊的字符串。
但其實說了那么多,簡單來說只有一點,就是我們只能通過反射對象來修改指針類型的變量。如果拿不到實際存儲數據的指針,那么我們就無法通過反射對象來修改其中的元素了。
原文鏈接:https://juejin.cn/post/7183998435245162552
相關推薦
- 2024-01-06 RocketMQ消息丟失問題
- 2022-08-19 關于springboot項目中引用jquery不生效的問題的解決方案
- 2022-05-31 Python?turtle.right與turtle.setheading的區別講述_python
- 2022-12-10 C++中的結構體vector排序問題_C 語言
- 2022-03-24 .Net?Core微服務網關Ocelot超時、熔斷、限流_自學過程
- 2023-10-11 MP、MybatisPlus、聯表查詢、自定義sql、Constants.WRAPPER、ew (二
- 2022-07-10 Spring依賴注入的幾種方式詳解
- 2023-02-01 MongoDB?事務支持詳解_MongoDB
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支