一文帶你了解Golang中reflect反射的常見(jiàn)錯(cuò)誤
go 的反射是很脆弱的,保證反射代碼正確運(yùn)行的前提是,在調(diào)用反射對(duì)象的方法之前, 先問(wèn)一下自己正在調(diào)用的方法是不是適合于所有用于創(chuàng)建反射對(duì)象的原始類(lèi)型。 go 反射的錯(cuò)誤大多數(shù)都來(lái)自于調(diào)用了一個(gè)不適合當(dāng)前類(lèi)型的方法(比如在一個(gè)整型反射對(duì)象上調(diào)用 Field() 方法)。 而且,這些錯(cuò)誤通常是在運(yùn)行時(shí)才會(huì)暴露出來(lái),而不是在編譯時(shí),如果我們傳遞的類(lèi)型在反射代碼中沒(méi)有被覆蓋到那么很容易就會(huì) panic。
本文就介紹一下使用 go 反射時(shí)很大概率會(huì)出現(xiàn)的錯(cuò)誤。
獲取 Value 的值之前沒(méi)有判斷類(lèi)型
對(duì)于 reflect.Value,我們有很多方法可以獲取它的值,比如 Int()、String() 等等。 但是,這些方法都有一個(gè)前提,就是反射對(duì)象底層必須是我們調(diào)用的那個(gè)方法對(duì)應(yīng)的類(lèi)型,否則會(huì) panic,比如下面這個(gè)例子:
var f float32 = 1.0 v := reflect.ValueOf(f) // 報(bào)錯(cuò):panic: reflect: call of reflect.Value.Int on float32 Value fmt.Println(v.Int())
上面這個(gè)例子中,f 是一個(gè) float32 類(lèi)型的浮點(diǎn)數(shù),然后我們嘗試通過(guò) Int() 方法來(lái)獲取一個(gè)整數(shù),但是這個(gè)方法只能用于 int 類(lèi)型的反射對(duì)象,所以會(huì)報(bào)錯(cuò)。
- 涉及的方法:
Addr,Bool,Bytes,Complex,Int,Uint,Float,Interface;調(diào)用這些方法的時(shí)候,如果類(lèi)型不對(duì)則會(huì)panic。 - 判斷反射對(duì)象能否轉(zhuǎn)換為某一類(lèi)型的方法:
CanAddr,CanInterface,CanComplex,CanFloat,CanInt,CanUint。 - 其他類(lèi)型是否能轉(zhuǎn)換判斷方法:
CanConvert,可以判斷一個(gè)反射對(duì)象能否轉(zhuǎn)換為某一類(lèi)型。
通過(guò) CanConvert 方法來(lái)判斷一個(gè)反射對(duì)象能否轉(zhuǎn)換為某一類(lèi)型:
// true fmt.Println(v.CanConvert(reflect.TypeOf(1.0)))
如果我們想將反射對(duì)象轉(zhuǎn)換為我們的自定義類(lèi)型,就可以通過(guò) CanConvert 來(lái)判斷是否能轉(zhuǎn)換,然后再調(diào)用 Convert 方法來(lái)轉(zhuǎn)換:
type Person struct {
Name string
}
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
v := reflect.ValueOf(p)
// v 可以轉(zhuǎn)換為 Person 類(lèi)型
assert.True(t, v.CanConvert(reflect.TypeOf(Person{})))
// v 可以轉(zhuǎn)換為 Person 類(lèi)型
p1 := v.Convert(reflect.TypeOf(Person{}))
assert.Equal(t, "foo", p1.Interface().(Person).Name)
}說(shuō)明:
reflect.TypeOf(Person{})可以取得Person類(lèi)型的信息v.Convert可以將v轉(zhuǎn)換為reflect.TypeOf(Person{})指定的類(lèi)型
沒(méi)有傳遞指針給 reflect.ValueOf
如果我們想通過(guò)反射對(duì)象來(lái)修改原變量,就必須傳遞一個(gè)指針,否則會(huì)報(bào)錯(cuò)(暫不考慮 slice, map, 結(jié)構(gòu)體字段包含指針字段的特殊情況):
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
v := reflect.ValueOf(p)
// 報(bào)錯(cuò):panic: reflect: reflect.Value.SetString using unaddressable value
v.FieldByName("Name").SetString("bar")
}這個(gè)錯(cuò)誤的原因是,v 是一個(gè) Person 類(lèi)型的值,而不是指針,所以我們不能通過(guò) v.FieldByName("Name") 來(lái)修改它的字段。
對(duì)于反射對(duì)象來(lái)說(shuō),只拿到了 p 的拷貝,而不是 p 本身,所以我們不能通過(guò)反射對(duì)象來(lái)修改 p。
在一個(gè)無(wú)效的 Value 上操作
我們有很多方法可以創(chuàng)建 reflect.Value,而且這類(lèi)方法沒(méi)有 error 返回值,這就意味著,就算我們創(chuàng)建 reflect.Value 的時(shí)候傳遞了一個(gè)無(wú)效的值,也不會(huì)報(bào)錯(cuò),而是會(huì)返回一個(gè)無(wú)效的 reflect.Value:
func TestReflect(t *testing.T) {
var p = Person{}
v := reflect.ValueOf(p)
// Person 不存在 foo 方法
// FieldByName 返回一個(gè)表示 Field 的反射對(duì)象 reflect.Value
v1 := v.FieldByName("foo")
assert.False(t, v1.IsValid())
// v1 是無(wú)效的,只有 String 方法可以調(diào)用
// 其他方法調(diào)用都會(huì) panic
assert.Panics(t, func() {
// panic: reflect: call of reflect.Value.NumMethod on zero Value
fmt.Println(v1.NumMethod())
})
}對(duì)于這個(gè)問(wèn)題,我們可以通過(guò) IsValid 方法來(lái)判斷 reflect.Value 是否有效:
func TestReflect(t *testing.T) {
var p = Person{}
v := reflect.ValueOf(p)
v1 := v.FieldByName("foo")
// 通過(guò) IsValid 判斷 reflect.Value 是否有效
if v1.IsValid() {
fmt.Println("p has foo field")
} else {
fmt.Println("p has no foo field")
}
}Field() 方法在傳遞的索引超出范圍的時(shí)候,直接 panic,而不會(huì)返回一個(gè) invalid 的 reflect.Value。
IsValid 報(bào)告反射對(duì)象 v 是否代表一個(gè)值。 如果 v 是零值,則返回 false。 如果 IsValid 返回 false,則除 String 之外的所有其他方法都將發(fā)生 panic。 大多數(shù)函數(shù)和方法從不返回?zé)o效值。
什么時(shí)候 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
在上面這個(gè)例子中,v 是有效的,它表示了一個(gè)指針,指針指向的對(duì)象為 nil。 但是 v.Elem() 和 reflect.Indirect(v) 都是無(wú)效的,因?yàn)樗鼈儽硎镜氖侵羔樦赶虻膶?duì)象,而指針指向的對(duì)象為 nil。 我們無(wú)法基于 nil 來(lái)做任何反射操作。
其他情況下 IsValid 返回 false
除了上面的情況,IsValid 還有其他情況下會(huì)返回 false:
- 空的反射值對(duì)象,獲取通過(guò)
nil創(chuàng)建的反射對(duì)象,其IsValid會(huì)返回false。 - 結(jié)構(gòu)體反射對(duì)象通過(guò)
FieldByName獲取了一個(gè)不存在的字段,其IsValid會(huì)返回false。 - 結(jié)構(gòu)體反射對(duì)象通過(guò)
MethodByName獲取了一個(gè)不存在的方法,其IsValid會(huì)返回false。 map反射對(duì)象通過(guò)MapIndex獲取了一個(gè)不存在的 key,其IsValid會(huì)返回false。
示例:
func TestReflect(t *testing.T) {
// 空的反射對(duì)象
fmt.Println(reflect.Value{}.IsValid()) // false
// 基于 nil 創(chuàng)建的反射對(duì)象
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())
}注意:還有其他一些情況也會(huì)使 IsValid 返回 false,這里只是列出了部分情況。 我們?cè)谑褂玫臅r(shí)候需要注意我們正在使用的反射對(duì)象會(huì)不會(huì)是無(wú)效的。
通過(guò)反射修改不可修改的值
對(duì)于 reflect.Value 對(duì)象,我們可以通過(guò) CanSet 方法來(lái)判斷它是否可以被設(shè)置:
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
// 傳遞值來(lái)創(chuàng)建的發(fā)射對(duì)象,
// 不能修改其值,因?yàn)樗且粋€(gè)副本
v := reflect.ValueOf(p)
assert.False(t, v.CanSet())
assert.False(t, v.Field(0).CanSet())
// 下面這一行代碼會(huì) panic:
// panic: reflect: reflect.Value.SetString using unaddressable value
// v.Field(0).SetString("bar")
// 指針?lè)瓷鋵?duì)象本身不能修改,
// 其指向的對(duì)象(也就是 v1.Elem())可以修改
v1 := reflect.ValueOf(&p)
assert.False(t, v1.CanSet())
assert.True(t, v1.Elem().CanSet())
}CanSet 報(bào)告 v 的值是否可以更改。只有可尋址(addressable)且不是通過(guò)使用未導(dǎo)出的結(jié)構(gòu)字段獲得的值才能更改。 如果 CanSet 返回 false,調(diào)用 Set 或任何類(lèi)型特定的 setter(例如 SetBool、SetInt)將 panic。CanSet 的條件是可尋址。
對(duì)于傳值創(chuàng)建的反射對(duì)象,我們無(wú)法通過(guò)反射對(duì)象來(lái)修改原變量,CanSet 方法返回 false。 例外的情況是,如果這個(gè)值中包含了指針,我們依然可以通過(guò)那個(gè)指針來(lái)修改其指向的對(duì)象。
只有通過(guò) Elem 方法的返回值才能設(shè)置指針指向的對(duì)象。
在錯(cuò)誤的 Value 上調(diào)用 Elem 方法
reflect.Value 的 Elem() 返回 interface 的反射對(duì)象包含的值或指針?lè)瓷鋵?duì)象指向的值。如果反射對(duì)象的 Kind 不是 reflect.Interface 或 reflect.Pointer,它會(huì)發(fā)生 panic。 如果反射對(duì)象為 nil,則返回零值。
我們知道,interface 類(lèi)型實(shí)際上包含了類(lèi)型和數(shù)據(jù)。而我們傳遞給 reflect.ValueOf 的參數(shù)就是 interface,所以在反射對(duì)象中也提供了方法來(lái)獲取 interface 類(lèi)型的類(lèi)型和數(shù)據(jù):
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
v := reflect.ValueOf(p)
// 下面這一行會(huì)報(bào)錯(cuò):
// panic: reflect: call of reflect.Value.Elem on struct Value
// v.Elem()
fmt.Println(v.Type())
// v1 是 *Person 類(lèi)型的反射對(duì)象,是一個(gè)指針
v1 := reflect.ValueOf(&p)
fmt.Println(v1.Elem(), v1.Type())
}在上面的例子中,v 是一個(gè) Person 類(lèi)型的反射對(duì)象,它不是一個(gè)指針,所以我們不能通過(guò) v.Elem() 來(lái)獲取它指向的對(duì)象。 而 v1 是一個(gè)指針,所以我們可以通過(guò) v1.Elem() 來(lái)獲取它指向的對(duì)象。
調(diào)用了一個(gè)其類(lèi)型不能調(diào)用的方法
這可能是最常見(jiàn)的一類(lèi)錯(cuò)誤了,因?yàn)樵?go 的反射系統(tǒng)中,我們調(diào)用的一些方法又會(huì)返回一個(gè)相同類(lèi)型的反射對(duì)象,但是這個(gè)新的反射對(duì)象可能是一個(gè)不同的類(lèi)型了。同時(shí)返回的這個(gè)反射對(duì)象是否有效也是未知的。
在 go 中,反射有兩大對(duì)象 reflect.Type 和 reflect.Value,它們都存在一些方法只適用于某些特定的類(lèi)型,也就是說(shuō), 在 go 的反射設(shè)計(jì)中,只分為了類(lèi)型和值兩大類(lèi)。但是實(shí)際的 go 中的類(lèi)型就有很多種,比如 int、string、struct、interface、slice、map、chan、func 等等。
我們先不說(shuō) reflect.Type,我們從 reflect.Value 的角度看看,將這么多類(lèi)型的值都抽象為 reflect.Value 之后, 我們?nèi)绾潍@取某些類(lèi)型值特定的信息呢?比如獲取結(jié)構(gòu)體的某一個(gè)字段的值,或者調(diào)用某一個(gè)方法。 這個(gè)問(wèn)題很好解決,需要獲取結(jié)構(gòu)體字段是吧,那給你提供一個(gè) Field() 方法,需要調(diào)用方法吧,那給你提供一個(gè) Call() 方法。
但是這樣一來(lái),有另外一個(gè)問(wèn)題就是,如果我們的 reflect.Value 是從一個(gè) int 類(lèi)型的值創(chuàng)建的, 那么我們調(diào)用 Field() 方法就會(huì)發(fā)生 panic,因?yàn)?int 類(lèi)型的值是沒(méi)有 Field() 方法的:
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
v := reflect.ValueOf(p)
// 獲取反射對(duì)象的 Name 字段
assert.Equal(t, "foo", v.Field(0).String())
var i = 1
v1 := reflect.ValueOf(i)
assert.Panics(t, func() {
// 下面這一行會(huì) panic:
// v1 沒(méi)有 Field 方法
fmt.Println(v1.Field(0).String())
})
}至于有哪些方法是某些類(lèi)型特定的,可以參考一下下面兩個(gè)文檔:
總結(jié)
- 在調(diào)用
Int()、Float()等方法時(shí),需要確保反射對(duì)象的類(lèi)型是正確的類(lèi)型,否則會(huì)panic,比如在一個(gè)flaot類(lèi)型的反射對(duì)象上調(diào)用Int()方法就會(huì)panic。 - 如果想修改原始的變量,創(chuàng)建
reflect.Value時(shí)需要傳入原始變量的指針。 - 如果
reflect.Value的IsValid()方法返回false,那么它就是一個(gè)無(wú)效的反射對(duì)象,調(diào)用它的任何方法都會(huì)panic,除了String方法。 - 對(duì)于基于值創(chuàng)建的
reflect.Value,如果想要修改它的值,我們無(wú)法調(diào)用這個(gè)反射對(duì)象的Set*方法,因?yàn)樾薷囊粋€(gè)變量的拷貝沒(méi)有任何意義。 - 同時(shí),我們也無(wú)法通過(guò)
reflect.Value去修改結(jié)構(gòu)體中未導(dǎo)出的字段,即使我們創(chuàng)建reflect.Value時(shí)傳入的是結(jié)構(gòu)體的指針。 Elem()只可以在指針或者interface類(lèi)型的反射對(duì)象上調(diào)用,否則會(huì)panic,它的作用是獲取指針指向的對(duì)象的反射對(duì)象,又或者獲取接口data的反射對(duì)象。reflect.Value和reflect.Type都有很多類(lèi)型特定的方法,比如Field()、Call()等,這些方法只能在某些類(lèi)型的反射對(duì)象上調(diào)用,否則會(huì)panic。
到此這篇關(guān)于一文帶你了解Golang中reflect反射的常見(jiàn)錯(cuò)誤的文章就介紹到這了,更多相關(guān)Golang reflect反射內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言Swagger實(shí)現(xiàn)為項(xiàng)目生成 API 文檔
Swagger 是一個(gè)基于 OpenAPI 規(guī)范設(shè)計(jì)的工具,用于為 RESTful API 生成交互式文檔,下面小編就來(lái)介紹一下如何在 Go 項(xiàng)目中集成 Swagger,特別是結(jié)合 Gin 框架生成 API 文檔2025-03-03
Linux環(huán)境下編譯并運(yùn)行g(shù)o項(xiàng)目的全過(guò)程
Go語(yǔ)言是Google的開(kāi)源編程語(yǔ)言,廣泛應(yīng)用于云計(jì)算、分布式系統(tǒng)開(kāi)發(fā)等領(lǐng)域,在Linux上也有大量的應(yīng)用場(chǎng)景,這篇文章主要給大家介紹了關(guān)于Linux環(huán)境下編譯并運(yùn)行g(shù)o項(xiàng)目的相關(guān)資料,需要的朋友可以參考下2023-11-11
深入淺出Go:掌握基礎(chǔ)知識(shí)的關(guān)鍵要點(diǎn)
Go是一種開(kāi)源的編程語(yǔ)言,由Google開(kāi)發(fā),它具有簡(jiǎn)潔、高效、并發(fā)性強(qiáng)的特點(diǎn),適用于構(gòu)建可靠的、高性能的軟件系統(tǒng),本文將介紹Go的基礎(chǔ)知識(shí),需要的朋友可以參考下2023-10-10

