一文帶你了解Golang中reflect反射的常見錯誤
go 的反射是很脆弱的,保證反射代碼正確運行的前提是,在調(diào)用反射對象的方法之前, 先問一下自己正在調(diào)用的方法是不是適合于所有用于創(chuàng)建反射對象的原始類型。 go 反射的錯誤大多數(shù)都來自于調(diào)用了一個不適合當(dāng)前類型的方法(比如在一個整型反射對象上調(diào)用 Field()
方法)。 而且,這些錯誤通常是在運行時才會暴露出來,而不是在編譯時,如果我們傳遞的類型在反射代碼中沒有被覆蓋到那么很容易就會 panic
。
本文就介紹一下使用 go 反射時很大概率會出現(xiàn)的錯誤。
獲取 Value 的值之前沒有判斷類型
對于 reflect.Value
,我們有很多方法可以獲取它的值,比如 Int()
、String()
等等。 但是,這些方法都有一個前提,就是反射對象底層必須是我們調(diào)用的那個方法對應(yīng)的類型,否則會 panic
,比如下面這個例子:
var f float32 = 1.0 v := reflect.ValueOf(f) // 報錯:panic: reflect: call of reflect.Value.Int on float32 Value fmt.Println(v.Int())
上面這個例子中,f
是一個 float32
類型的浮點數(shù),然后我們嘗試通過 Int()
方法來獲取一個整數(shù),但是這個方法只能用于 int
類型的反射對象,所以會報錯。
- 涉及的方法:
Addr
,Bool
,Bytes
,Complex
,Int
,Uint
,Float
,Interface
;調(diào)用這些方法的時候,如果類型不對則會panic
。 - 判斷反射對象能否轉(zhuǎn)換為某一類型的方法:
CanAddr
,CanInterface
,CanComplex
,CanFloat
,CanInt
,CanUint
。 - 其他類型是否能轉(zhuǎn)換判斷方法:
CanConvert
,可以判斷一個反射對象能否轉(zhuǎn)換為某一類型。
通過 CanConvert
方法來判斷一個反射對象能否轉(zhuǎn)換為某一類型:
// true fmt.Println(v.CanConvert(reflect.TypeOf(1.0)))
如果我們想將反射對象轉(zhuǎn)換為我們的自定義類型,就可以通過 CanConvert
來判斷是否能轉(zhuǎn)換,然后再調(diào)用 Convert
方法來轉(zhuǎn)換:
type Person struct { Name string } func TestReflect(t *testing.T) { p := Person{Name: "foo"} v := reflect.ValueOf(p) // v 可以轉(zhuǎn)換為 Person 類型 assert.True(t, v.CanConvert(reflect.TypeOf(Person{}))) // v 可以轉(zhuǎn)換為 Person 類型 p1 := v.Convert(reflect.TypeOf(Person{})) assert.Equal(t, "foo", p1.Interface().(Person).Name) }
說明:
reflect.TypeOf(Person{})
可以取得Person
類型的信息v.Convert
可以將v
轉(zhuǎn)換為reflect.TypeOf(Person{})
指定的類型
沒有傳遞指針給 reflect.ValueOf
如果我們想通過反射對象來修改原變量,就必須傳遞一個指針,否則會報錯(暫不考慮 slice
, map
, 結(jié)構(gòu)體字段包含指針字段的特殊情況):
func TestReflect(t *testing.T) { p := Person{Name: "foo"} v := reflect.ValueOf(p) // 報錯:panic: reflect: reflect.Value.SetString using unaddressable value v.FieldByName("Name").SetString("bar") }
這個錯誤的原因是,v
是一個 Person
類型的值,而不是指針,所以我們不能通過 v.FieldByName("Name")
來修改它的字段。
對于反射對象來說,只拿到了 p 的拷貝,而不是 p 本身,所以我們不能通過反射對象來修改 p。
在一個無效的 Value 上操作
我們有很多方法可以創(chuàng)建 reflect.Value
,而且這類方法沒有 error
返回值,這就意味著,就算我們創(chuàng)建 reflect.Value
的時候傳遞了一個無效的值,也不會報錯,而是會返回一個無效的 reflect.Value
:
func TestReflect(t *testing.T) { var p = Person{} v := reflect.ValueOf(p) // Person 不存在 foo 方法 // FieldByName 返回一個表示 Field 的反射對象 reflect.Value v1 := v.FieldByName("foo") assert.False(t, v1.IsValid()) // v1 是無效的,只有 String 方法可以調(diào)用 // 其他方法調(diào)用都會 panic assert.Panics(t, func() { // panic: reflect: call of reflect.Value.NumMethod on zero Value fmt.Println(v1.NumMethod()) }) }
對于這個問題,我們可以通過 IsValid
方法來判斷 reflect.Value
是否有效:
func TestReflect(t *testing.T) { var p = Person{} v := reflect.ValueOf(p) v1 := v.FieldByName("foo") // 通過 IsValid 判斷 reflect.Value 是否有效 if v1.IsValid() { fmt.Println("p has foo field") } else { fmt.Println("p has no foo field") } }
Field() 方法在傳遞的索引超出范圍的時候,直接 panic,而不會返回一個 invalid 的 reflect.Value。
IsValid
報告反射對象 v
是否代表一個值。 如果 v
是零值,則返回 false
。 如果 IsValid
返回 false
,則除 String
之外的所有其他方法都將發(fā)生 panic
。 大多數(shù)函數(shù)和方法從不返回?zé)o效值。
什么時候 IsValid 返回 false
reflect.Value
的 IsValid
的返回值表示 reflect.Value
是否有效,而不是它代表的值是否有效。比如:
var b *int = nil v := reflect.ValueOf(b) fmt.Println(v.IsValid()) // true fmt.Println(v.Elem().IsValid()) // false fmt.Println(reflect.Indirect(v).IsValid()) // false
在上面這個例子中,v
是有效的,它表示了一個指針,指針指向的對象為 nil
。 但是 v.Elem()
和 reflect.Indirect(v)
都是無效的,因為它們表示的是指針指向的對象,而指針指向的對象為 nil
。 我們無法基于 nil
來做任何反射操作。
其他情況下 IsValid 返回 false
除了上面的情況,IsValid
還有其他情況下會返回 false
:
- 空的反射值對象,獲取通過
nil
創(chuàng)建的反射對象,其IsValid
會返回false
。 - 結(jié)構(gòu)體反射對象通過
FieldByName
獲取了一個不存在的字段,其IsValid
會返回false
。 - 結(jié)構(gòu)體反射對象通過
MethodByName
獲取了一個不存在的方法,其IsValid
會返回false
。 map
反射對象通過MapIndex
獲取了一個不存在的 key,其IsValid
會返回false
。
示例:
func TestReflect(t *testing.T) { // 空的反射對象 fmt.Println(reflect.Value{}.IsValid()) // false // 基于 nil 創(chuàng)建的反射對象 fmt.Println(reflect.ValueOf(nil).IsValid()) // false s := struct{}{} // 獲取不存在的字段 fmt.Println(reflect.ValueOf(s).FieldByName("").IsValid()) // false // 獲取不存在的方法 fmt.Println(reflect.ValueOf(s).MethodByName("").IsValid()) // false m := map[int]int{} // 獲取 map 的不存在的 key fmt.Println(reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid()) }
注意:還有其他一些情況也會使 IsValid
返回 false
,這里只是列出了部分情況。 我們在使用的時候需要注意我們正在使用的反射對象會不會是無效的。
通過反射修改不可修改的值
對于 reflect.Value
對象,我們可以通過 CanSet
方法來判斷它是否可以被設(shè)置:
func TestReflect(t *testing.T) { p := Person{Name: "foo"} // 傳遞值來創(chuàng)建的發(fā)射對象, // 不能修改其值,因為它是一個副本 v := reflect.ValueOf(p) assert.False(t, v.CanSet()) assert.False(t, v.Field(0).CanSet()) // 下面這一行代碼會 panic: // panic: reflect: reflect.Value.SetString using unaddressable value // v.Field(0).SetString("bar") // 指針反射對象本身不能修改, // 其指向的對象(也就是 v1.Elem())可以修改 v1 := reflect.ValueOf(&p) assert.False(t, v1.CanSet()) assert.True(t, v1.Elem().CanSet()) }
CanSet
報告 v
的值是否可以更改。只有可尋址(addressable
)且不是通過使用未導(dǎo)出的結(jié)構(gòu)字段獲得的值才能更改。 如果 CanSet
返回 false
,調(diào)用 Set
或任何類型特定的 setter
(例如 SetBool
、SetInt
)將 panic
。CanSet 的條件是可尋址。
對于傳值創(chuàng)建的反射對象,我們無法通過反射對象來修改原變量,CanSet
方法返回 false
。 例外的情況是,如果這個值中包含了指針,我們依然可以通過那個指針來修改其指向的對象。
只有通過 Elem 方法的返回值才能設(shè)置指針指向的對象。
在錯誤的 Value 上調(diào)用 Elem 方法
reflect.Value
的 Elem()
返回 interface
的反射對象包含的值或指針反射對象指向的值。如果反射對象的 Kind
不是 reflect.Interface
或 reflect.Pointer
,它會發(fā)生 panic
。 如果反射對象為 nil
,則返回零值。
我們知道,interface
類型實際上包含了類型和數(shù)據(jù)。而我們傳遞給 reflect.ValueOf
的參數(shù)就是 interface
,所以在反射對象中也提供了方法來獲取 interface
類型的類型和數(shù)據(jù):
func TestReflect(t *testing.T) { p := Person{Name: "foo"} v := reflect.ValueOf(p) // 下面這一行會報錯: // panic: reflect: call of reflect.Value.Elem on struct Value // v.Elem() fmt.Println(v.Type()) // v1 是 *Person 類型的反射對象,是一個指針 v1 := reflect.ValueOf(&p) fmt.Println(v1.Elem(), v1.Type()) }
在上面的例子中,v
是一個 Person
類型的反射對象,它不是一個指針,所以我們不能通過 v.Elem()
來獲取它指向的對象。 而 v1
是一個指針,所以我們可以通過 v1.Elem()
來獲取它指向的對象。
調(diào)用了一個其類型不能調(diào)用的方法
這可能是最常見的一類錯誤了,因為在 go 的反射系統(tǒng)中,我們調(diào)用的一些方法又會返回一個相同類型的反射對象,但是這個新的反射對象可能是一個不同的類型了。同時返回的這個反射對象是否有效也是未知的。
在 go 中,反射有兩大對象 reflect.Type
和 reflect.Value
,它們都存在一些方法只適用于某些特定的類型,也就是說, 在 go 的反射設(shè)計中,只分為了類型和值兩大類。但是實際的 go 中的類型就有很多種,比如 int
、string
、struct
、interface
、slice
、map
、chan
、func
等等。
我們先不說 reflect.Type
,我們從 reflect.Value
的角度看看,將這么多類型的值都抽象為 reflect.Value
之后, 我們?nèi)绾潍@取某些類型值特定的信息呢?比如獲取結(jié)構(gòu)體的某一個字段的值,或者調(diào)用某一個方法。 這個問題很好解決,需要獲取結(jié)構(gòu)體字段是吧,那給你提供一個 Field()
方法,需要調(diào)用方法吧,那給你提供一個 Call()
方法。
但是這樣一來,有另外一個問題就是,如果我們的 reflect.Value
是從一個 int
類型的值創(chuàng)建的, 那么我們調(diào)用 Field()
方法就會發(fā)生 panic
,因為 int
類型的值是沒有 Field()
方法的:
func TestReflect(t *testing.T) { p := Person{Name: "foo"} v := reflect.ValueOf(p) // 獲取反射對象的 Name 字段 assert.Equal(t, "foo", v.Field(0).String()) var i = 1 v1 := reflect.ValueOf(i) assert.Panics(t, func() { // 下面這一行會 panic: // v1 沒有 Field 方法 fmt.Println(v1.Field(0).String()) }) }
至于有哪些方法是某些類型特定的,可以參考一下下面兩個文檔:
總結(jié)
- 在調(diào)用
Int()
、Float()
等方法時,需要確保反射對象的類型是正確的類型,否則會panic
,比如在一個flaot
類型的反射對象上調(diào)用Int()
方法就會panic
。 - 如果想修改原始的變量,創(chuàng)建
reflect.Value
時需要傳入原始變量的指針。 - 如果
reflect.Value
的IsValid()
方法返回false
,那么它就是一個無效的反射對象,調(diào)用它的任何方法都會panic
,除了String
方法。 - 對于基于值創(chuàng)建的
reflect.Value
,如果想要修改它的值,我們無法調(diào)用這個反射對象的Set*
方法,因為修改一個變量的拷貝沒有任何意義。 - 同時,我們也無法通過
reflect.Value
去修改結(jié)構(gòu)體中未導(dǎo)出的字段,即使我們創(chuàng)建reflect.Value
時傳入的是結(jié)構(gòu)體的指針。 Elem()
只可以在指針或者interface
類型的反射對象上調(diào)用,否則會panic
,它的作用是獲取指針指向的對象的反射對象,又或者獲取接口data
的反射對象。reflect.Value
和reflect.Type
都有很多類型特定的方法,比如Field()
、Call()
等,這些方法只能在某些類型的反射對象上調(diào)用,否則會panic
。
到此這篇關(guān)于一文帶你了解Golang中reflect反射的常見錯誤的文章就介紹到這了,更多相關(guān)Golang reflect反射內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MacOS中 VSCode 安裝 GO 插件失敗問題的快速解決方法
這篇文章主要介紹了MacOS中 VSCode 安裝 GO 插件失敗問題的快速解決方法,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05解決goland 導(dǎo)入項目后import里的包報紅問題
這篇文章主要介紹了解決goland 導(dǎo)入項目后import里的包報紅問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05go HTTP2 的頭部壓縮算法hpack實現(xiàn)詳解
這篇文章主要為大家介紹了go HTTP2 的頭部壓縮算法hpack實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10Go語言高效I/O并發(fā)處理雙緩沖和Exchanger模式實例探索
這篇文章主要介紹了Go語言高效I/O并發(fā)處理雙緩沖和Exchanger模式實例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01go Antlr重構(gòu)腳本解釋器實現(xiàn)示例
這篇文章主要為大家介紹了go Antlr重構(gòu)腳本解釋器實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08Golang 實現(xiàn) Redis系列(六)如何實現(xiàn) pipeline 模式的 redis 客戶端
pipeline 模式的 redis 客戶端需要有兩個后臺協(xié)程負責(zé) tcp 通信,調(diào)用方通過 channel 向后臺協(xié)程發(fā)送指令,并阻塞等待直到收到響應(yīng),本文是使用 golang 實現(xiàn) redis 系列的第六篇, 將介紹如何實現(xiàn)一個 Pipeline 模式的 Redis 客戶端。2021-07-07