網站首頁 編程語言 正文
go 的反射是很脆弱的,保證反射代碼正確運行的前提是,在調用反射對象的方法之前, 先問一下自己正在調用的方法是不是適合于所有用于創建反射對象的原始類型。 go 反射的錯誤大多數都來自于調用了一個不適合當前類型的方法(比如在一個整型反射對象上調用 Field()
方法)。 而且,這些錯誤通常是在運行時才會暴露出來,而不是在編譯時,如果我們傳遞的類型在反射代碼中沒有被覆蓋到那么很容易就會 panic
。
本文就介紹一下使用 go 反射時很大概率會出現的錯誤。
獲取 Value 的值之前沒有判斷類型
對于 reflect.Value
,我們有很多方法可以獲取它的值,比如 Int()
、String()
等等。 但是,這些方法都有一個前提,就是反射對象底層必須是我們調用的那個方法對應的類型,否則會 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
類型的浮點數,然后我們嘗試通過 Int()
方法來獲取一個整數,但是這個方法只能用于 int
類型的反射對象,所以會報錯。
- 涉及的方法:
Addr
,Bool
,Bytes
,Complex
,Int
,Uint
,Float
,Interface
;調用這些方法的時候,如果類型不對則會panic
。 - 判斷反射對象能否轉換為某一類型的方法:
CanAddr
,CanInterface
,CanComplex
,CanFloat
,CanInt
,CanUint
。 - 其他類型是否能轉換判斷方法:
CanConvert
,可以判斷一個反射對象能否轉換為某一類型。
通過 CanConvert
方法來判斷一個反射對象能否轉換為某一類型:
// true fmt.Println(v.CanConvert(reflect.TypeOf(1.0)))
如果我們想將反射對象轉換為我們的自定義類型,就可以通過 CanConvert
來判斷是否能轉換,然后再調用 Convert
方法來轉換:
type Person struct { Name string } func TestReflect(t *testing.T) { p := Person{Name: "foo"} v := reflect.ValueOf(p) // v 可以轉換為 Person 類型 assert.True(t, v.CanConvert(reflect.TypeOf(Person{}))) // v 可以轉換為 Person 類型 p1 := v.Convert(reflect.TypeOf(Person{})) assert.Equal(t, "foo", p1.Interface().(Person).Name) }
說明:
-
reflect.TypeOf(Person{})
可以取得Person
類型的信息 -
v.Convert
可以將v
轉換為reflect.TypeOf(Person{})
指定的類型
沒有傳遞指針給 reflect.ValueOf
如果我們想通過反射對象來修改原變量,就必須傳遞一個指針,否則會報錯(暫不考慮 slice
, map
, 結構體字段包含指針字段的特殊情況):
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 上操作
我們有很多方法可以創建 reflect.Value
,而且這類方法沒有 error
返回值,這就意味著,就算我們創建 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 方法可以調用 // 其他方法調用都會 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
之外的所有其他方法都將發生 panic
。 大多數函數和方法從不返回無效值。
什么時候 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
創建的反射對象,其IsValid
會返回false
。 - 結構體反射對象通過
FieldByName
獲取了一個不存在的字段,其IsValid
會返回false
。 - 結構體反射對象通過
MethodByName
獲取了一個不存在的方法,其IsValid
會返回false
。 -
map
反射對象通過MapIndex
獲取了一個不存在的 key,其IsValid
會返回false
。
示例:
func TestReflect(t *testing.T) { // 空的反射對象 fmt.Println(reflect.Value{}.IsValid()) // false // 基于 nil 創建的反射對象 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
方法來判斷它是否可以被設置:
func TestReflect(t *testing.T) { p := Person{Name: "foo"} // 傳遞值來創建的發射對象, // 不能修改其值,因為它是一個副本 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
)且不是通過使用未導出的結構字段獲得的值才能更改。 如果 CanSet
返回 false
,調用 Set
或任何類型特定的 setter
(例如 SetBool
、SetInt
)將 panic
。CanSet 的條件是可尋址。
對于傳值創建的反射對象,我們無法通過反射對象來修改原變量,CanSet
方法返回 false
。 例外的情況是,如果這個值中包含了指針,我們依然可以通過那個指針來修改其指向的對象。
只有通過 Elem 方法的返回值才能設置指針指向的對象。
在錯誤的 Value 上調用 Elem 方法
reflect.Value
的 Elem()
返回 interface
的反射對象包含的值或指針反射對象指向的值。如果反射對象的 Kind
不是 reflect.Interface
或 reflect.Pointer
,它會發生 panic
。 如果反射對象為 nil
,則返回零值。
我們知道,interface
類型實際上包含了類型和數據。而我們傳遞給 reflect.ValueOf
的參數就是 interface
,所以在反射對象中也提供了方法來獲取 interface
類型的類型和數據:
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()
來獲取它指向的對象。
調用了一個其類型不能調用的方法
這可能是最常見的一類錯誤了,因為在 go 的反射系統中,我們調用的一些方法又會返回一個相同類型的反射對象,但是這個新的反射對象可能是一個不同的類型了。同時返回的這個反射對象是否有效也是未知的。
在 go 中,反射有兩大對象 reflect.Type
和 reflect.Value
,它們都存在一些方法只適用于某些特定的類型,也就是說, 在 go 的反射設計中,只分為了類型和值兩大類。但是實際的 go 中的類型就有很多種,比如 int
、string
、struct
、interface
、slice
、map
、chan
、func
等等。
我們先不說 reflect.Type
,我們從 reflect.Value
的角度看看,將這么多類型的值都抽象為 reflect.Value
之后, 我們如何獲取某些類型值特定的信息呢?比如獲取結構體的某一個字段的值,或者調用某一個方法。 這個問題很好解決,需要獲取結構體字段是吧,那給你提供一個 Field()
方法,需要調用方法吧,那給你提供一個 Call()
方法。
但是這樣一來,有另外一個問題就是,如果我們的 reflect.Value
是從一個 int
類型的值創建的, 那么我們調用 Field()
方法就會發生 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()) }) }
至于有哪些方法是某些類型特定的,可以參考一下下面兩個文檔:
- 類型特定的 reflect.Value 方法
- 類型特定的 reflect.Type 方法
總結
- 在調用
Int()
、Float()
等方法時,需要確保反射對象的類型是正確的類型,否則會panic
,比如在一個flaot
類型的反射對象上調用Int()
方法就會panic
。 - 如果想修改原始的變量,創建
reflect.Value
時需要傳入原始變量的指針。 - 如果
reflect.Value
的IsValid()
方法返回false
,那么它就是一個無效的反射對象,調用它的任何方法都會panic
,除了String
方法。 - 對于基于值創建的
reflect.Value
,如果想要修改它的值,我們無法調用這個反射對象的Set*
方法,因為修改一個變量的拷貝沒有任何意義。 - 同時,我們也無法通過
reflect.Value
去修改結構體中未導出的字段,即使我們創建reflect.Value
時傳入的是結構體的指針。 -
Elem()
只可以在指針或者interface
類型的反射對象上調用,否則會panic
,它的作用是獲取指針指向的對象的反射對象,又或者獲取接口data
的反射對象。 -
reflect.Value
和reflect.Type
都有很多類型特定的方法,比如Field()
、Call()
等,這些方法只能在某些類型的反射對象上調用,否則會panic
。
原文鏈接:https://juejin.cn/post/7184726072544477243
相關推薦
- 2022-11-01 C++中的pair使用詳解_C 語言
- 2022-06-02 Pandas實現DataFrame的簡單運算、統計與排序_python
- 2022-05-27 Flutter組件狀態管理的3種方法_Android
- 2022-06-15 Python文件的壓縮與解壓_python
- 2022-10-26 Python?Pyinstaller庫安裝步驟以及使用方法_python
- 2024-01-13 nvm命令
- 2022-08-04 react項目優化配置的操作詳解_React
- 2022-12-05 C++?Boost?MultiArray簡化使用多維數組庫_C 語言
- 最近更新
-
- 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同步修改后的遠程分支