日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無先后,達(dá)者為師

網(wǎng)站首頁 編程語言 正文

一文帶你了解Golang中reflect反射的常見錯(cuò)誤_Golang

作者:eleven26 ? 更新時(shí)間: 2023-02-12 編程語言

go 的反射是很脆弱的,保證反射代碼正確運(yùn)行的前提是,在調(diào)用反射對(duì)象的方法之前, 先問一下自己正在調(diào)用的方法是不是適合于所有用于創(chuàng)建反射對(duì)象的原始類型。 go 反射的錯(cuò)誤大多數(shù)都來自于調(diào)用了一個(gè)不適合當(dāng)前類型的方法(比如在一個(gè)整型反射對(duì)象上調(diào)用 Field() 方法)。 而且,這些錯(cuò)誤通常是在運(yùn)行時(shí)才會(huì)暴露出來,而不是在編譯時(shí),如果我們傳遞的類型在反射代碼中沒有被覆蓋到那么很容易就會(huì) panic

本文就介紹一下使用 go 反射時(shí)很大概率會(huì)出現(xiàn)的錯(cuò)誤。

獲取 Value 的值之前沒有判斷類型

對(duì)于 reflect.Value,我們有很多方法可以獲取它的值,比如 Int()String() 等等。 但是,這些方法都有一個(gè)前提,就是反射對(duì)象底層必須是我們調(diào)用的那個(gè)方法對(duì)應(yīng)的類型,否則會(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 類型的浮點(diǎn)數(shù),然后我們嘗試通過 Int() 方法來獲取一個(gè)整數(shù),但是這個(gè)方法只能用于 int 類型的反射對(duì)象,所以會(huì)報(bào)錯(cuò)。

  • 涉及的方法:Addr, Bool, Bytes, Complex, Int, Uint, Float, Interface;調(diào)用這些方法的時(shí)候,如果類型不對(duì)則會(huì) panic
  • 判斷反射對(duì)象能否轉(zhuǎn)換為某一類型的方法:CanAddr, CanInterface, CanComplex, CanFloat, CanInt, CanUint
  • 其他類型是否能轉(zhuǎn)換判斷方法:CanConvert,可以判斷一個(gè)反射對(duì)象能否轉(zhuǎn)換為某一類型。

通過 CanConvert 方法來判斷一個(gè)反射對(duì)象能否轉(zhuǎn)換為某一類型:

// true
fmt.Println(v.CanConvert(reflect.TypeOf(1.0)))

如果我們想將反射對(duì)象轉(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

如果我們想通過反射對(duì)象來修改原變量,就必須傳遞一個(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 類型的值,而不是指針,所以我們不能通過 v.FieldByName("Name") 來修改它的字段。

對(duì)于反射對(duì)象來說,只拿到了 p 的拷貝,而不是 p 本身,所以我們不能通過反射對(duì)象來修改 p。

在一個(gè)無效的 Value 上操作

我們有很多方法可以創(chuàng)建 reflect.Value,而且這類方法沒有 error 返回值,這就意味著,就算我們創(chuàng)建 reflect.Value 的時(shí)候傳遞了一個(gè)無效的值,也不會(huì)報(bào)錯(cuò),而是會(huì)返回一個(gè)無效的 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 是無效的,只有 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è)問題,我們可以通過 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() 方法在傳遞的索引超出范圍的時(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.ValueIsValid 的返回值表示 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) 都是無效的,因?yàn)樗鼈儽硎镜氖侵羔樦赶虻膶?duì)象,而指針指向的對(duì)象為 nil。 我們無法基于 nil 來做任何反射操作。

其他情況下 IsValid 返回 false

除了上面的情況,IsValid 還有其他情況下會(huì)返回 false

  • 空的反射值對(duì)象,獲取通過 nil 創(chuàng)建的反射對(duì)象,其 IsValid 會(huì)返回 false
  • 結(jié)構(gòu)體反射對(duì)象通過 FieldByName 獲取了一個(gè)不存在的字段,其 IsValid 會(huì)返回 false
  • 結(jié)構(gòu)體反射對(duì)象通過 MethodByName 獲取了一個(gè)不存在的方法,其 IsValid 會(huì)返回 false
  • map 反射對(duì)象通過 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ì)是無效的。

通過反射修改不可修改的值

對(duì)于 reflect.Value 對(duì)象,我們可以通過 CanSet 方法來判斷它是否可以被設(shè)置:

func TestReflect(t *testing.T) {
   p := Person{Name: "foo"}

   // 傳遞值來創(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")

   // 指針反射對(duì)象本身不能修改,
   // 其指向的對(duì)象(也就是 v1.Elem())可以修改
   v1 := reflect.ValueOf(&p)
   assert.False(t, v1.CanSet())
   assert.True(t, v1.Elem().CanSet())
}

CanSet 報(bào)告 v 的值是否可以更改。只有可尋址(addressable)且不是通過使用未導(dǎo)出的結(jié)構(gòu)字段獲得的值才能更改。 如果 CanSet 返回 false,調(diào)用 Set 或任何類型特定的 setter(例如 SetBoolSetInt)將 panicCanSet 的條件是可尋址。

對(duì)于傳值創(chuàng)建的反射對(duì)象,我們無法通過反射對(duì)象來修改原變量,CanSet 方法返回 false例外的情況是,如果這個(gè)值中包含了指針,我們依然可以通過那個(gè)指針來修改其指向的對(duì)象。

只有通過 Elem 方法的返回值才能設(shè)置指針指向的對(duì)象。

在錯(cuò)誤的 Value 上調(diào)用 Elem 方法

reflect.ValueElem() 返回 interface 的反射對(duì)象包含的值或指針反射對(duì)象指向的值。如果反射對(duì)象的 Kind 不是 reflect.Interfacereflect.Pointer,它會(huì)發(fā)生 panic。 如果反射對(duì)象為 nil,則返回零值。

我們知道,interface 類型實(shí)際上包含了類型和數(shù)據(jù)。而我們傳遞給 reflect.ValueOf 的參數(shù)就是 interface,所以在反射對(duì)象中也提供了方法來獲取 interface 類型的類型和數(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 類型的反射對(duì)象,是一個(gè)指針
   v1 := reflect.ValueOf(&p)
   fmt.Println(v1.Elem(), v1.Type())
}

在上面的例子中,v 是一個(gè) Person 類型的反射對(duì)象,它不是一個(gè)指針,所以我們不能通過 v.Elem() 來獲取它指向的對(duì)象。 而 v1 是一個(gè)指針,所以我們可以通過 v1.Elem() 來獲取它指向的對(duì)象。

調(diào)用了一個(gè)其類型不能調(diào)用的方法

這可能是最常見的一類錯(cuò)誤了,因?yàn)樵?go 的反射系統(tǒng)中,我們調(diào)用的一些方法又會(huì)返回一個(gè)相同類型的反射對(duì)象,但是這個(gè)新的反射對(duì)象可能是一個(gè)不同的類型了。同時(shí)返回的這個(gè)反射對(duì)象是否有效也是未知的。

在 go 中,反射有兩大對(duì)象 reflect.Typereflect.Value,它們都存在一些方法只適用于某些特定的類型,也就是說, 在 go 的反射設(shè)計(jì)中,只分為了類型兩大類。但是實(shí)際的 go 中的類型就有很多種,比如 intstringstructinterfaceslicemapchanfunc 等等。

我們先不說 reflect.Type,我們從 reflect.Value 的角度看看,將這么多類型的值都抽象為 reflect.Value 之后, 我們?nèi)绾潍@取某些類型值特定的信息呢?比如獲取結(jié)構(gòu)體的某一個(gè)字段的值,或者調(diào)用某一個(gè)方法。 這個(gè)問題很好解決,需要獲取結(jié)構(gòu)體字段是吧,那給你提供一個(gè) Field() 方法,需要調(diào)用方法吧,那給你提供一個(gè) Call() 方法。

但是這樣一來,有另外一個(gè)問題就是,如果我們的 reflect.Value 是從一個(gè) int 類型的值創(chuàng)建的, 那么我們調(diào)用 Field() 方法就會(huì)發(fā)生 panic,因?yàn)?int 類型的值是沒有 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 沒有 Field 方法
      fmt.Println(v1.Field(0).String())
   })
}

至于有哪些方法是某些類型特定的,可以參考一下下面兩個(gè)文檔:

  • 類型特定的 reflect.Value 方法
  • 類型特定的 reflect.Type 方法

總結(jié)

  • 在調(diào)用 Int()Float() 等方法時(shí),需要確保反射對(duì)象的類型是正確的類型,否則會(huì) panic,比如在一個(gè) flaot 類型的反射對(duì)象上調(diào)用 Int() 方法就會(huì) panic
  • 如果想修改原始的變量,創(chuàng)建 reflect.Value 時(shí)需要傳入原始變量的指針。
  • 如果 reflect.ValueIsValid() 方法返回 false,那么它就是一個(gè)無效的反射對(duì)象,調(diào)用它的任何方法都會(huì) panic,除了 String 方法。
  • 對(duì)于基于值創(chuàng)建的 reflect.Value,如果想要修改它的值,我們無法調(diào)用這個(gè)反射對(duì)象的 Set* 方法,因?yàn)樾薷囊粋€(gè)變量的拷貝沒有任何意義。
  • 同時(shí),我們也無法通過 reflect.Value 去修改結(jié)構(gòu)體中未導(dǎo)出的字段,即使我們創(chuàng)建 reflect.Value 時(shí)傳入的是結(jié)構(gòu)體的指針。
  • Elem() 只可以在指針或者 interface 類型的反射對(duì)象上調(diào)用,否則會(huì) panic,它的作用是獲取指針指向的對(duì)象的反射對(duì)象,又或者獲取接口 data 的反射對(duì)象。
  • reflect.Valuereflect.Type 都有很多類型特定的方法,比如 Field()Call() 等,這些方法只能在某些類型的反射對(duì)象上調(diào)用,否則會(huì) panic

原文鏈接:https://juejin.cn/post/7184726072544477243

欄目分類
最近更新