自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

解密 Go 語言之反射 Reflect

開發(fā) 后端
在所有的語言中,反射這一功能基本屬于必不可少的模塊。雖說 “反射” 這個詞讓人根深蒂固,但更多的還是 WHY。反射到底是什么,反射又是基于什么法則實現(xiàn)的?

[[361315]]

本文轉(zhuǎn)載自微信公眾號「腦子進煎魚了」,作者陳煎魚 。轉(zhuǎn)載本文請聯(lián)系腦子進煎魚了公眾號。

 大家好,我是煎魚。今天是 2020 年的最后一天,讓我們一起繼續(xù)愉快的學習吧 :)。

在所有的語言中,反射這一功能基本屬于必不可少的模塊。

雖說 “反射” 這個詞讓人根深蒂固,但更多的還是 WHY。反射到底是什么,反射又是基于什么法則實現(xiàn)的?

今天我們通過這篇文章來一一揭曉,以 Go 語言為例,了解反射到底為何物,其底層又是如何實現(xiàn)的。

反射是什么

在計算機學中,反射是指計算機程序在運行時(runtime)可以訪問、檢測和修改它本身狀態(tài)或行為的一種能力。

用比喻來說,反射就是程序在運行的時候能夠 “觀察” 并且修改自己的行為(來自維基百科)。

簡單來講就是,應用程序能夠在運行時觀察到變量的值,并且能夠修改他。

一個例子

最常見的 reflect 標準庫例子,如下:

  1. import ( 
  2.  "fmt" 
  3.  "reflect" 
  4.  
  5. func main() { 
  6.  rv := []interface{}{"hi", 42, func() {}} 
  7.  for _, v := range rv { 
  8.   switch v := reflect.ValueOf(v); v.Kind() { 
  9.   case reflect.String: 
  10.    fmt.Println(v.String()) 
  11.   case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 
  12.    fmt.Println(v.Int()) 
  13.   default
  14.    fmt.Printf("unhandled kind %s", v.Kind()) 
  15.   } 
  16.  } 

輸出結(jié)果:

  1. hi 
  2. 42 
  3. unhandled kind func 

在程序中主要是聲明了 rv 變量,變量類型為 interface{},其包含 3 個不同類型的值,分別是字符串、數(shù)字、閉包。

而在使用 interface{} 時常見于不知道入?yún)⒄呔唧w的基本類型是什么,那么我們就會用 interface{} 類型來做一個偽 “泛型”。

此時又會引出一個新的問題,既然入?yún)⑹?interface{},那么出參時呢?

Go 語言是強類型語言,入?yún)⑹?interface{},出參也肯定是跑不了的,因此必然離不開類型的判斷,這時候就要用到反射,也就是 reflect 標準庫。反射過后又再進行 (type) 的類型斷言。

這就是我們在編寫程序時最常遇見的一個反射使用場景。

Go reflect

reflect 標準庫中,最核心的莫過于 reflect.Type 和 reflect.Value 類型。而在反射中所使用的方法都圍繞著這兩者進行,其方法主要含義如下:

  • TypeOf 方法:用于提取入?yún)⒅档念愋托畔ⅰ?/li>
  • ValueOf 方法:用于提取存儲的變量的值信息。

reflect.TypeOf

演示程序:

  1. func main() { 
  2.  blog := Blog{"煎魚"
  3.  typeof := reflect.TypeOf(blog) 
  4.  fmt.Println(typeof.String()) 

輸出結(jié)果:

  1. main.Blog 

從輸出結(jié)果中,可得出 reflect.TypeOf 成功解析出 blog 變量的類型是 main.Blog,也就是連 package 都知道了。

通過人識別的角度來看似乎很正常,但程序就不是這樣了。他是怎么知道 “他” 是哪個 package 下的什么呢?

我們一起追一下源碼看看:

  1. func TypeOf(i interface{}) Type { 
  2.  eface := *(*emptyInterface)(unsafe.Pointer(&i)) 
  3.  return toType(eface.typ) 

從源碼層面來看,TypeOf 方法中主要涉及三塊操作,分別如下:

  1. 使用 unsafe.Pointer 方法獲取任意類型且可尋址的指針值。
  2. 利用 emptyInterface 類型進行強制的 interface 類型轉(zhuǎn)換。
  3. 調(diào)用 toType 方法轉(zhuǎn)換為可供外部使用的 Type 類型。

而這之中信息量最大的是 emptyInterface 結(jié)構(gòu)體中的 rtype 類型:

  1. type rtype struct { 
  2.  size       uintptr 
  3.  ptrdata    uintptr  
  4.  hash       uint32  
  5.  tflag      tflag  
  6.  align      uint8   
  7.  fieldAlign uint8   
  8.  kind       uint8    
  9.  equal     func(unsafe.Pointer, unsafe.Pointer) bool 
  10.  gcdata    *byte   
  11.  str       nameOff  
  12.  ptrToThis typeOff  

在使用上最重要的是 rtype 類型,其實現(xiàn)了 Type 類型的所有接口方法,因此他可以直接作為 Type 類型返回。

而 Type 本質(zhì)上是一個接口實現(xiàn),其包含了獲取一個類型所必要的所有方法:

  1. type Type interface { 
  2.  // 適用于所有類型 
  3.  // 返回該類型內(nèi)存對齊后所占用的字節(jié)數(shù) 
  4.  Align() int 
  5.  
  6.  // 僅作用于 strcut 類型 
  7.  // 返回該類型內(nèi)存對齊后所占用的字節(jié)數(shù) 
  8.  FieldAlign() int 
  9.  
  10.  // 返回該類型的方法集中的第 i 個方法 
  11.  Method(int) Method 
  12.  
  13.  // 根據(jù)方法名獲取對應方法集中的方法 
  14.  MethodByName(string) (Method, bool) 
  15.  
  16.  // 返回該類型的方法集中導出的方法的數(shù)量。 
  17.  NumMethod() int 
  18.  
  19.  // 返回該類型的名稱 
  20.  Name() string 
  21.  ... 

建議大致過一遍,了解清楚有哪些方法,再針對向看就好。

主體思想是給自己大腦建立一個索引,便于后續(xù)快速到 pkg.go.dev 上查詢即可。

reflect.ValueOf

演示程序:

  1. func main() { 
  2.  var x float64 = 3.4 
  3.  fmt.Println("value:", reflect.ValueOf(x)) 

輸出結(jié)果:

  1. value: 3.4 

從輸出結(jié)果中,可得知通過 reflect.ValueOf 成功獲取到了變量 x 的值為 3.4。與 reflect.TypeOf 形成一個相匹配,一個負責獲取類型,一個負責獲取值。

那么 reflect.ValueOf 是怎么獲取到值的呢,核心源碼如下:

  1. func ValueOf(i interface{}) Value { 
  2.  if i == nil { 
  3.   return Value{} 
  4.  } 
  5.  
  6.  escapes(i) 
  7.  
  8.  return unpackEface(i) 
  9.  
  10. func unpackEface(i interface{}) Value { 
  11.  e := (*emptyInterface)(unsafe.Pointer(&i)) 
  12.  t := e.typ 
  13.  if t == nil { 
  14.   return Value{} 
  15.  } 
  16.  f := flag(t.Kind()) 
  17.  if ifaceIndir(t) { 
  18.   f |= flagIndir 
  19.  } 
  20.  return Value{t, e.word, f} 

從源碼層面來看,ValueOf 方法中主要涉及如下幾個操作:

  1. 調(diào)用 escapes 讓變量 i 逃逸到堆上。
  2. 將變量 i 強制轉(zhuǎn)換為 emptyInterface 類型。
  3. 將所需的信息(其中包含值的具體類型和指針)組裝成 reflect.Value 類型后返回。

何時類型轉(zhuǎn)換

在調(diào)用 reflect 進行一系列反射行為時,Go 又是在什么時候進行的類型轉(zhuǎn)換呢?

畢竟我們傳入的是 float64,而函數(shù)如參數(shù)是 inetrface 類型。

查看匯編如下:

  1. $ go tool compile -S main.go                          
  2.  ... 
  3.  0x0058 00088 ($GOROOT/src/reflect/value.go:2817) LEAQ type.float64(SB), CX 
  4.  0x005f 00095 ($GOROOT/src/reflect/value.go:2817) MOVQ CX, reflect.dummy+8(SB) 
  5.  0x0066 00102 ($GOROOT/src/reflect/value.go:2817) PCDATA $0, $-2 
  6.  0x0066 00102 ($GOROOT/src/reflect/value.go:2817) CMPL runtime.writeBarrier(SB), $0 
  7.  0x006d 00109 ($GOROOT/src/reflect/value.go:2817) JNE 357 
  8.  0x0073 00115 ($GOROOT/src/reflect/value.go:2817) MOVQ AX, reflect.dummy+16(SB) 
  9.  0x007a 00122 ($GOROOT/src/reflect/value.go:2348) PCDATA $0, $-1 
  10.  0x007a 00122 ($GOROOT/src/reflect/value.go:2348) MOVQ CX, reflect.i+64(SP) 
  11.  0x007f 00127 ($GOROOT/src/reflect/value.go:2348) MOVQ AX, reflect.i+72(SP) 
  12.  ... 

顯然,Go 語言會在編譯階段就會完成分析,且進行類型轉(zhuǎn)換。這樣子 reflect 真正所使用的就是 interface 類型了。

reflect.Set

演示程序:

  1. func main() { 
  2.  i := 2.33 
  3.  v := reflect.ValueOf(&i) 
  4.  v.Elem().SetFloat(6.66) 
  5.  log.Println("value: ", i) 

輸出結(jié)果:

  1. value:  6.66 

從輸出結(jié)果中,我們可得知在調(diào)用 reflect.ValueOf 方法后,我們利用 SetFloat 方法進行了值變更。

核心的方法之一就是 Setter 相關的方法,我們可以一起看看其源碼是怎么實現(xiàn)的:

  1. func (v Value) Set(x Value) { 
  2.  v.mustBeAssignable() 
  3.  x.mustBeExported() // do not let unexported x leak 
  4.  var target unsafe.Pointer 
  5.  if v.kind() == Interface { 
  6.   target = v.ptr 
  7.  } 
  8.  x = x.assignTo("reflect.Set", v.typ, target) 
  9.  if x.flag&flagIndir != 0 { 
  10.   typedmemmove(v.typ, v.ptr, x.ptr) 
  11.  } else { 
  12.   *(*unsafe.Pointer)(v.ptr) = x.ptr 
  13.  } 
  1. 檢查反射對象及其字段是否可以被設置。
  2. 檢查反射對象及其字段是否導出(對外公開)。
  3. 調(diào)用 assignTo 方法創(chuàng)建一個新的反射對象并對原本的反射對象進行覆蓋。
  4. 根據(jù) assignTo 方法所返回的指針值,對當前反射對象的指針進行值的修改。

簡單來講就是,檢查是否可以設置,接著創(chuàng)建一個新的對象,最后對其修改。是一個非常標準的賦值流程。

反射三大定律

Go 語言中的反射,其歸根究底都是在實現(xiàn)三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

我們將針對這核心的三大定律進行介紹和說明,以此來理解 Go 反射里的各種方法是基于什么理念實現(xiàn)的。

第一定律

反射的第一定律是:“反射可以從接口值(interface)得到反射對象”。

示例代碼:

  1. func main() { 
  2.  var x float64 = 3.4 
  3.  fmt.Println("type:", reflect.TypeOf(x)) 

輸出結(jié)果:

  1. type: float64 

可能有讀者就迷糊了,我明明在代碼中傳入的變量 x,他的類型是 float64。怎么就成從接口值得到反射對象了。

其實不然,雖然在代碼中我們所傳入的變量基本類型是 float64,但是 reflect.TypeOf 方法入?yún)⑹?interface{},本質(zhì)上 Go 語言內(nèi)部對其是做了類型轉(zhuǎn)換的。這一塊會在后面會進一步展開說明。

第二定律

反射的第二定律是:“可以從反射對象得到接口值(interface)”。其與第一條定律是相反的定律,可以是互相補充了。

示例代碼:

  1. func main() { 
  2.  vo := reflect.ValueOf(3.4) 
  3.  vf := vo.Interface().(float64) 
  4.  log.Println("value:", vf) 

輸出結(jié)果:

  1. value: 3.4 

可以看到在示例代碼中,變量 vo 已經(jīng)是反射對象,然后我們可以利用其所提供的的 Interface 方法獲取到接口值(interface),并最后強制轉(zhuǎn)換回我們原始的變量類型。

第三定律

反射的第三定律是:“要修改反射對象,該值必須可以修改”。第三條定律看上去與第一、第二條均無直接關聯(lián),但卻是必不可少的,因為反射在工程實踐中,目的一就是可以獲取到值和類型,其二就是要能夠修改他的值。

否則反射出來只能看,不能動,就會造成這個反射很雞肋。例如:應用程序中的配置熱更新,必然會涉及配置項相關的變量變動,大多會使用到反射來變動初始值。

示例代碼:

  1. func main() { 
  2.  i := 2.33 
  3.  v := reflect.ValueOf(&i) 
  4.  v.Elem().SetFloat(6.66) 
  5.  log.Println("value: ", i) 

輸出結(jié)果:

  1. value:  6.66 

單從結(jié)果來看,變量 i 的值確實從 2.33 變成了 6.66,似乎非常完美。

但是單看代碼,似乎有些 “問題”,怎么設置一個反射值這么 ”麻煩“:

為什么必須傳入變量 i 的指針引用?

為什么變量 v 在設置前還需要 Elem 一下?

本叛逆的 Gophper 表示我就不這么設置,行不行呢,會不會出現(xiàn)什么問題:

  1. func main() { 
  2.  i := 2.33 
  3.  reflect.ValueOf(i).SetFloat(6.66) 
  4.  log.Println("value: ", i) 

報錯信息:

  1. panic: reflect: reflect.Value.SetFloat using unaddressable value 
  2.  
  3. goroutine 1 [running]: 
  4. reflect.flag.mustBeAssignableSlow(0x8e) 
  5.         /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:259 +0x138 
  6. reflect.flag.mustBeAssignable(...) 
  7.         /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:246 
  8. reflect.Value.SetFloat(0x10b2980, 0xc00001a0b0, 0x8e, 0x401aa3d70a3d70a4) 
  9.         /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:1609 +0x37 
  10. main.main() 
  11.         /Users/eddycjy/go-application/awesomeProject/main.go:10 +0xc5 

根據(jù)上述提示可知,由于使用 “使用不可尋址的值”,因此示例程序無法正常的運作下去。并且這是一個 reflect 標準庫本身就加以防范了的硬性要求。

這么做的原因在于,Go 語言的函數(shù)調(diào)用的傳遞都是值拷貝的,因此若不傳指針引用,單純值傳遞,那么肯定是無法變動反射對象的源值的。因此 Go 標準庫就對其進行了邏輯判斷,避免出現(xiàn)問題。

因此期望變更反射對象的源值時,我們必須主動傳入對應變量的指針引用,并且調(diào)用 reflect 標準庫的 Elem 方法來獲取指針所指向的源變量,并且最后調(diào)用 Set 相關方法來進行設置。

總結(jié)

通過本文我們學習并了解了 Go 反射是如何使用,又是基于什么定律設計的。另外我們稍加關注,不難發(fā)現(xiàn) Go 的反射都是基于接口(interface)來實現(xiàn)的,更進一步來講,Go 語言中運行時的功能很多都是基于接口來實現(xiàn)的。

整體來講,Go 反射是圍繞著三者進行的,分別是 Type、Value 以及 Interface,三者相輔相成,而反射本質(zhì)上與 Interface 存在直接關系,Interface 這一塊的內(nèi)容我們也將在后續(xù)的文章進行進一步的剖析。

 

責任編輯:武曉燕 來源: 腦子進煎魚了
相關推薦

2021-05-12 08:53:54

Go語言調(diào)度

2021-10-23 06:42:14

Go語言接口

2021-10-03 22:18:14

Go語言整數(shù)

2021-10-16 17:53:35

Go函數(shù)編程

2021-10-09 07:52:01

Go程序重命名

2022-03-28 13:34:26

Go泛型部署泛型

2024-05-10 08:15:32

go語言反射機制

2011-04-01 14:50:56

Java的反射機制

2024-01-05 20:46:14

2013-08-20 10:11:20

Go系統(tǒng)管理員

2021-10-18 10:53:26

Go 代碼技術(shù)

2024-01-08 08:23:07

Go語言代碼

2021-04-30 09:04:11

Go 語言結(jié)構(gòu)體type

2013-07-10 11:11:05

PythonGo語言

2012-08-13 14:13:46

2017-06-14 09:37:05

R語言Apriori算法

2018-08-01 15:10:02

GolangPython語言

2012-02-13 10:03:31

編程開發(fā)

2020-09-24 10:50:53

加密解密語言hmac

2020-12-16 08:07:28

語言基礎反射
點贊
收藏

51CTO技術(shù)棧公眾號