一文詳解Golang中的反射

2022-12-14 22:00:25
本篇文章帶大家主要來聊聊Golang中反射,希望對你有新的認知。

php入門到就業線上直播課:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

雖然很多人使用 Go 語言有一定時間了,甚至有的使用了 1 年 2 年,然後對於 Go 語言中的反射還是模稜兩可,使用起來的時候,心裡也不是非常有底氣。【相關推薦:Go視訊教學、】

更有甚者,幾乎不使用反射,當然,也不是什麼錯,在工作中能用最簡單最高效,又可延伸,效能還好的方式來進行處理自然是最 nice ,沒有必要去生搬硬套一些高階用法,畢竟工作不是我們的試煉場,可以自己下來多多實驗,本次就來好好看看如何去玩反射

文章分別從如下五個方面來聊

  • 反射是什麼
  • 反射的規則
  • 使用案例並靈活運用
  • 反射原理
  • 總結

簡單來看反射是什麼

簡單來看,反射就是在程式執行時期對程式本身進行存取和修改的能力,例如在程式執行時,可以修改程式的欄位名稱,欄位值,還可以給程式提供介面存取的資訊等等

這是 Go 語言中提供的一種機制,我們可以在 Go 語言公共庫中可以看到很多關於 reflect 的使用位置

例如常用的 fmt 包,常用的 json 序列化和反序列化,自然前面我們說到的 gorm 庫自然也是使用了反射的

可是我們一般為什麼要使用反射呢?

根據反射的能力,自然是因為我們提供的介面並不知道傳入的資料型別會是什麼樣的, 只有當程式執行的時候才知道具體的資料型別

但是我們編碼的時候又期望去校驗程式執行時傳入的型別會是什麼樣的(例如 json 的序列化)並對其這種具體的資料進行操作,這個時候,咱們就需要用到反射的能力了

所以對於使用到反射的地方,你都能看到 interface{} 是不是就不奇怪了呢?

正是因為不確定傳入的資料型別會是什麼樣的,所以才設計成 interface{} ,那麼如果對於 interface 有還不清楚他的特點和使用方式的,可以檢視歷史文章:

先關注反射的規則

首先關注反射的三個重要的定律,知道規則之後,我們按照規則玩就不會有什麼問題,只有當我們不清楚規則,總是觸發條款的時候,才會出現奇奇怪怪的問題

  • 反射是可以將 介面型別的變數 轉換成 反射型別的物件

  • 反射可以將 反射型別的物件 轉換成 介面型別的變數
  • 我們在執行時要去修改的 反射型別的物件 ,那麼要求這個物件對應的值是要可寫的

對於上述 3 個規則也是比較好理解,還記的之前我們說過的 unsafe 包裡面的指標嗎?

都是將我們常用的資料型別,轉換成包(例如 unsafe包,或者 reflect 包)裡面的指定資料型別,然後再按照包裡面的規則進行修改資料

相當於,換個馬甲,就可以進行不同的操作了

關注使用案例並靈活運用

一般咱們先會基本的應用,再去研究他的原理,研究他為什麼可以這樣用,慢慢的才能理解的更加深刻

對於定律一,將 介面型別的變數 轉換成 反射型別的物件

實際上此處說的 介面型別的變數 我們可以傳入任意資料型別的變數,例如 int, float, string ,map, slice, struct 等等

反射型別的物件 這裡就可以理解成 reflect 反射包中的 reflect.Type reflect.Value 物件,可以通過 reflect 包中提供的 TypeOfValueOf 函數得到

其中 reflect.Type 實際上是一個 interface ,他裡面包含了各種介面需要進行實現,它裡面提供了關於型別相關的資訊

其中如下圖可以檢視到 reflect.Type 的所有方法,其中

  • 綠色的 是所有資料型別都是可以呼叫的
  • 紅色的是 函數型別資料可以呼叫的
  • 黑色的是 Map,陣列 Array,通道 Chan,指標 Ptr 或者 切片Slice 可以呼叫的
  • 藍色的是結構體呼叫的
  • 黃色的是通道 channel 型別呼叫的

reflect.Value 實際上是一個 struct,根據這個 struct 還關聯了一組方法,這裡面存放了資料型別和具體的資料,通過檢視其資料結構就可以看出

type Value struct {
   typ *rtype
   ptr unsafe.Pointer
   flag
}
登入後複製

看到此處的 unsafe.Pointer 是不是很熟悉,底層自然就可以將 unsafe.Pointer 轉換成 uintptr,然後再修改其資料後,再轉換回來,對於 Go 指標不太熟悉的可以檢視這篇文章:

寫一個簡單的 demo 就可以簡單的獲取到變數的資料型別和值

func main() {   var demoStr string = "now reflect"
   fmt.Println("type:", reflect.TypeOf(demoStr))
   fmt.Println("value:", reflect.ValueOf(demoStr))
}
登入後複製

對於定律二,將 反射型別的物件 轉換成 介面型別的變數

我們可以通過將 reflect.Value 型別轉換成我們具體的資料型別,因為 reflect.Value 中有對應的 typ *rtype 以及 ptr unsafe.Pointer

例如我們可以 通過 reflect.Value 物件的 interface() 方法來處理

func main() {   var demoStr string = "now reflect"
   fmt.Println("type:", reflect.TypeOf(demoStr))
   fmt.Println("value:", reflect.ValueOf(demoStr))   var res string
   res = reflect.ValueOf(demoStr).Interface().(string)
   fmt.Println("res == ",res)
}
登入後複製

對於定律三,修改反射型別的物件

首先我們看上書的 demo 程式碼,傳入 TypeOfValueOf 的變數實際上也是一個拷貝,那麼如果期望在反射型別的物件中修改其值,那麼就需要拿到具體變數的地址然後再進行修改,前提是這個變數是可寫的

舉個例子你就能明白

func main() {
   var demoStr string = "now reflect"
   v := reflect.ValueOf(demoStr)
   fmt.Println("is canset ", v.CanSet())
   //v.SetString("hello world")   // 會panic
   }
登入後複製

可以先呼叫 reflect.Value 物件的 CanSet 檢視是否可寫,如果是可寫的,我們再寫,如果不可寫就不要寫了,否則會 panic

那麼傳入變數的地址就可以修改了??

傳入地址的思路沒有毛病,但是我們去設定值的方式有問題,因此也會出現上述的 panic 情況

此處仔細看能夠明白,反射的物件 v 自然是不可修改的,我們應該找到 reflect.Value 裡面具體具體的資料指標,那麼才是可以修改的,可以使用 reflect.Value Elem 方法

稍微複雜一點的

看上了上述案例可能會覺得那麼簡單的案例,一演示就 ok,但是工作中一用就崩潰,那自然還是沒有融會貫通,說明還沒有消化好,再來一個工作中的例子

  • 一個結構體裡面有 map,map 中的 key 是 string,value 是 []string
  • 需求是存取 結構體中 hobby 欄位對應的 map key 為 sport 的切片的第1 個元素,並將其修改為 hellolworld
type RDemo struct {
   Name  string
   Age   int
   Money float32
   Hobby map[string][]string
}

func main() {
   tmp := &RDemo{
      Name:  "xiaomiong",
      Age:   18,
      Money: 25.6,
      Hobby: map[string][]string{
         "sport": {"basketball", "football"},
         "food":  {"beef"},
      },
   }

   v := reflect.ValueOf(tmp).Elem()  // 拿到結構體物件
   h := v.FieldByName("Hobby")    // 拿到 Hobby 物件
   h1 := h.MapKeys()[0]    // 拿到 Hobby 的第 0 個key
   fmt.Println("key1 name == ",h1.Interface().(string))

   sli := h.MapIndex(h1)    // 拿到 Hobby 的第 0 個key對應的物件
   str := sli.Index(1)      // 拿到切片的第 1 個物件
   fmt.Println(str.CanSet())

   str.SetString("helloworld")
   fmt.Println("tmp == ",tmp)
}
登入後複製

可以看到上述案例執行之後有時可以執行成功,有時會出現 panic 的情況,相信細心的 xdm 就可以看出來,是因為 map 中的 key 是 無序的導致的,此處也提醒一波,使用 map 的時候要注意這一點

看上述程式碼,是不是就能夠明白咱們使用反射去找到對應的資料型別,然後按照資料型別進行處理資料的過程了呢

有需要的話,可以慢慢的去熟練反射包中涉及的函數,重點是要了解其三個規則,物件轉換方式,存取方式,以及資料修改方式

反射原理

那麼通過上述案例,可以知道關於反射中資料型別和資料指標對應的值是相當重要的,不同的資料型別能夠用哪些函數這個需要注意,否則用錯直接就會 panic

TypeOf

來看 TypeOf 的介面中涉及的資料結構

在 reflect 包中 rtype 是非常重要的,Go 中所有的型別都會包含這個結構,所以咱們反射可以應用起來,結構如下

// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
   size       uintptr
   ptrdata    uintptr
   hash       uint32 
   tflag      tflag
   align      uint8
   fieldAlign uint8
   kind       uint8
   equal     func(unsafe.Pointer, unsafe.Pointer) bool
   gcdata    *byte 
   str       nameOff
   ptrToThis typeOff
}
登入後複製

其中可以看到此處的 rtype 的結構保持和 runtime/type.go 一致 ,都是關於資料型別的表示,以及對應的指標,關於這一塊的說明和演示可以檢視文末的 interface{} 處的內容

ValueOf

ValueOf 的原始碼中,我們可以看到,重要的是 emptyInterface 結構

// emptyInterface is the header for an interface{} value.type emptyInterface struct {
   typ  *rtype
   word unsafe.Pointer
}複製程式碼
登入後複製

emptyInterface 結構中有 rtype 型別的指標, word 自然是對應的資料的地址了

reflect.Value 物件中的方法也是非常的多,用起來和上述說到的 reflect.Type 介面中的功能類似

關於原始碼中涉及到的方法,就不再過多的贅述了,更多的還是需要自己多多實踐才能體會的更好

殊不知,此處的 reflect.Value 也是可以轉換成 reflect.Type ,可以檢視原始碼中 reflect\value.gofunc (v Value) Type() Type {

其中 reflect.Valuereflect.Type ,和任意資料型別 可以相互這樣來轉換

如下圖:

總結

至此,關於反射就聊到這裡,一些關於原始碼的細節並沒有詳細說,更多的站在一個使用者的角度去看反射需要注意的點

關於反射,大多的人是建議少用,因為是會影響到效能,不過如果不太關注這一點,那麼用起來還是非常方便的

高階功能自然也是雙刃劍,你用不好就會 panic,如果你期望去使用他,那麼就去更多的深入瞭解和一步一步的吃透他吧

大道至簡,反射三定律,活學活用

更多程式設計相關知識,請存取:!!

以上就是一文詳解Golang中的反射的詳細內容,更多請關注TW511.COM其它相關文章!