一文帶你了解Golang中reflect反射的常見錯誤
go 的反射是很脆弱的,保證反射代碼正確運行的前提是,在調(diào)用反射對象的方法之前, 先問一下自己正在調(diào)用的方法是不是適合于所有用于創(chuàng)建反射對象的原始類型。 go 反射的錯誤大多數(shù)都來自于調(diào)用了一個不適合當前類型的方法(比如在一個整型反射對象上調(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)文章
Go語言Swagger實現(xiàn)為項目生成 API 文檔
Swagger 是一個基于 OpenAPI 規(guī)范設(shè)計的工具,用于為 RESTful API 生成交互式文檔,下面小編就來介紹一下如何在 Go 項目中集成 Swagger,特別是結(jié)合 Gin 框架生成 API 文檔2025-03-03
Linux環(huán)境下編譯并運行g(shù)o項目的全過程
Go語言是Google的開源編程語言,廣泛應(yīng)用于云計算、分布式系統(tǒng)開發(fā)等領(lǐng)域,在Linux上也有大量的應(yīng)用場景,這篇文章主要給大家介紹了關(guān)于Linux環(huán)境下編譯并運行g(shù)o項目的相關(guān)資料,需要的朋友可以參考下2023-11-11

