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

學無先后,達者為師

網站首頁 編程語言 正文

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

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

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.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

在上面這個例子中,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)將 panicCanSet 的條件是可尋址。

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

只有通過 Elem 方法的返回值才能設置指針指向的對象。

在錯誤的 Value 上調用 Elem 方法

reflect.ValueElem() 返回 interface 的反射對象包含的值或指針反射對象指向的值。如果反射對象的 Kind 不是 reflect.Interfacereflect.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.Typereflect.Value,它們都存在一些方法只適用于某些特定的類型,也就是說, 在 go 的反射設計中,只分為了類型兩大類。但是實際的 go 中的類型就有很多種,比如 int、stringstruct、interface、slicemap、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.ValueIsValid() 方法返回 false,那么它就是一個無效的反射對象,調用它的任何方法都會 panic,除了 String 方法。
  • 對于基于值創建的 reflect.Value,如果想要修改它的值,我們無法調用這個反射對象的 Set* 方法,因為修改一個變量的拷貝沒有任何意義。
  • 同時,我們也無法通過 reflect.Value 去修改結構體中未導出的字段,即使我們創建 reflect.Value 時傳入的是結構體的指針。
  • Elem() 只可以在指針或者 interface 類型的反射對象上調用,否則會 panic,它的作用是獲取指針指向的對象的反射對象,又或者獲取接口 data 的反射對象。
  • reflect.Valuereflect.Type 都有很多類型特定的方法,比如 Field()、Call() 等,這些方法只能在某些類型的反射對象上調用,否則會 panic。

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

欄目分類
最近更新