反射和 Interface 息息相關,而 Interface 是我們上一篇文章的內容。在開始正文前,和大家說點題外話。
上一篇關於 Interface 的文章發出後,獲得了很多的關註和閱讀。比如,登上了 GoCN 的每日新聞第一條:
可能是編輯者覺得這篇文章稱不上“深度解密”,把標題給小小地改動了下,哈哈~~
在部落格園登上了 48 小時閱讀排行榜:
在開發者頭條 APP (類似於今日頭條,不過內容都是技術相關的,還挺有意思的)上收穫了 150 收藏,並被推薦到首頁最顯眼的 banner 位置,閱讀量達到了 1w 多,只是不知道這個數字是否是真實的,有點難以相信。
很多同學在後臺向我反映文章太長了,不利於閱讀,建議拆分一下。我非常理解,讀屏時代,大家需要快速地讀完全文,拿到收益。而碼農桃花源的文章都非常長,讀者很難在短時間內讀完,並且獲得相應的收益。
首先非常感謝大家的建議!其實我的想法是這樣的:大家都在說,現在是一個資訊嚴重過載的時代,資訊多得看不完,不免產生很多焦慮。但是,我想說,優質的資訊真有那麼多嗎?在我看來,文章的水平都是參差不齊,很多毫無內容和價值,大家把時間浪費在這些資訊上面是很不值得的。因為你讀了這些文章,就沒有了讀其他好的文章的精力。
所以,碼農桃花源想做一個優質資訊源,提供優質的內容。每一篇文章都是有深度,有內容,有收穫。一篇文章,我一般得花費 2 周左右,算是半月更,和那些日更的沒法比。當然,只是在數量上沒法比。而這個時代,最不缺的就是數量。
另外,文章長也算是我的一個特色。我完全可以拆分成上、中、下等等,但我希望一次性交付給我的讀者所有有價值的內容。這樣,你可以集中一個小時或是更長時間,精讀完一篇文章。
微信公眾號閱讀文章有時不是太方便,這裡推薦大家用微信讀書,直接搜尋公眾號名字就可以看到公眾號所有的文章了,很方便。這個功能出來很久了,我也是不久前才知道,推薦給大家。
閑話結束,今天要講的內容是反射,進入正題。下麵是文章的提綱:
什麼是反射為什麼要用反射反射是如何實現的types 和 interface反射的基本函式反射的三大定律反射相關函式的使用程式碼樣例未匯出成員反射的實際應用json 序列化DeepEqual 的作用及原理總結參考資料 |
---|
什麼是反射
直接看維基百科上的定義:
在電腦科學中,反射是指計算機程式在執行時(Run time)可以訪問、檢測和修改它本身狀態或行為的一種能力。用比喻來說,反射就是程式在執行的時候能夠“觀察”並且修改自己的行為。
那我就要問個問題了:不用反射就不能在執行時訪問、檢測和修改它本身的狀態和行為嗎?
問題的回答,其實要首先理解什麼叫訪問、檢測和修改它本身狀態或行為,它的本質是什麼?
實際上,它的本質是程式在執行期探知物件的型別資訊和記憶體結構,不用反射能行嗎?可以的!使用組合語言,直接和內層打交道,什麼資訊不能獲取?但是,當程式設計遷移到高階語言上來之後,就不行了!就只能透過 反射
來達到此項技能。
不同語言的反射模型不盡相同,有些語言還不支援反射。《Go 語言聖經》中是這樣定義反射的:
Go 語言提供了一種機制在執行時更新變數和檢查它們的值、呼叫它們的方法,但是在編譯時並不知道這些變數的具體型別,這稱為反射機制。
為什麼要用反射
需要反射的 2 個常見場景:
-
有時你需要編寫一個函式,但是並不知道傳給你的引數型別是什麼,可能是沒約定好;也可能是傳入的型別很多,這些型別並不能統一表示。這時反射就會用的上了。
-
有時候需要根據某些條件決定呼叫哪個函式,比如根據使用者的輸入來決定。這時就需要對函式和函式的引數進行反射,在執行期間動態地執行函式。
在講反射的原理以及如何用之前,還是說幾點不使用反射的理由:
-
與反射相關的程式碼,經常是難以閱讀的。在軟體工程中,程式碼可讀性也是一個非常重要的指標。
-
Go 語言作為一門靜態語言,編碼過程中,編譯器能提前發現一些型別錯誤,但是對於反射程式碼是無能為力的。所以包含反射相關的程式碼,很可能會執行很久,才會出錯,這時候經常是直接 panic,可能會造成嚴重的後果。
-
反射對效能影響還是比較大的,比正常程式碼執行速度慢一到兩個數量級。所以,對於一個專案中處於執行效率關鍵位置的程式碼,儘量避免使用反射特性。
反射是如何實現的
上一篇文章講到了 interface
,它是 Go 語言實現抽象的一個非常強大的工具。當向介面變數賦予一個物體型別的時候,介面會儲存物體的型別資訊,反射就是透過介面的型別資訊實現的,反射建立在型別的基礎上。
Go 語言在 reflect 包裡定義了各種型別,實現了反射的各種函式,透過它們可以在執行時檢測型別的資訊、改變型別的值。
types 和 interface
Go 語言中,每個變數都有一個靜態型別,在編譯階段就確定了的,比如 int,float64,[]int
等等。註意,這個型別是宣告時候的型別,不是底層資料型別。
Go 官方部落格裡就舉了一個例子:
-
type MyInt int
-
-
var i int
-
var j MyInt
儘管 i,j 的底層型別都是 int,但我們知道,他們是不同的靜態型別,除非進行型別轉換,否則,i 和 j 不能同時出現在等號兩側。j 的靜態型別就是 MyInt
。
反射主要與 interface{} 型別相關。前面一篇關於 interface 相關的文章已經探討過 interface 的底層結構,這裡再來複習一下。
-
type iface struct {
-
tab *itab
-
data unsafe.Pointer
-
}
-
-
type itab struct {
-
inter *interfacetype
-
_type *_type
-
link *itab
-
hash uint32
-
bad bool
-
inhash bool
-
unused [2]byte
-
fun [1]uintptr
-
}
其中 itab
由具體型別 _type
以及 interfacetype
組成。 _type
表示具體型別,而 interfacetype
則表示具體型別實現的介面型別。
實際上,iface 描述的是非空介面,它包含方法;與之相對的是 eface
,描述的是空介面,不包含任何方法,Go 語言裡有的型別都 “實現了”
空介面。
-
type eface struct {
-
_type *_type
-
data unsafe.Pointer
-
}
相比 iface
, eface
就比較簡單了。只維護了一個 _type
欄位,表示空介面所承載的具體的物體型別。 data
描述了具體的值。
還是用 Go 官方關於反射的部落格裡的例子,當然,我會用圖形來詳細解釋,結合兩者來看會更清楚。順便提一下,搞技術的不要害怕英文資料,要想成為技術專家,讀英文原始資料是技術提高的一條必經之路。
先明確一點:介面變數可以儲存任何實現了介面定義的所有方法的變數。
Go 語言中最常見的就是 Reader
和 Writer
介面:
-
type Reader interface {
-
Read(p []byte) (n int, err error)
-
}
-
-
type Writer interface {
-
Write(p []byte) (n int, err error)
-
}
接下來,就是介面之間的各種轉換和賦值了:
-
var r io.Reader
-
tty, err := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)
-
if err != nil {
-
return nil, err
-
}
-
r = tty
首先宣告 r
的型別是 io.Reader
,註意,這是 r
的靜態型別,此時它的動態型別為 nil
,並且它的動態值也是 nil
。
之後, r=tty
這一陳述句,將 r
的動態型別變成 *os.File
,動態值則變成非空,表示開啟的檔案物件。這時,r 可以用 對來表示為:
。
註意看上圖,此時雖然 fun
所指向的函式只有一個 Read
函式,其實 *os.File
還包含 Write
函式,也就是說 *os.File
其實還實現了 io.Writer
介面。因此下麵的斷言陳述句可以執行:
-
var w io.Writer
-
w = r.(io.Writer)
之所以用斷言,而不能直接賦值,是因為 r
的靜態型別是 io.Reader
,並沒有實現 io.Writer
介面。斷言能否成功,看 r
的動態型別是否符合要求。
這樣,w 也可以表示成 ,僅管它和
w
一樣,但是 w 可呼叫的函式取決於它的靜態型別 io.Writer
,也就是說它只能有這樣的呼叫形式: w.Write()
。 w
的記憶體形式如下圖:
和 w
相比,僅僅是 fun
對應的函式變了: Read->Write
。
最後,再來一個賦值:
-
var empty interface{}
-
empty = w
由於 empty
是一個空介面,因此所有的型別都實現了它,w 可以直接賦給它,不需要執行斷言操作。
從上面的三張圖可以看到,interface 包含三部分資訊: _type
是型別資訊, *data
指向實際型別的實際值, itab
包含實際型別的資訊,包括大小、包路徑,還包含系結在型別上的各種方法(圖上沒有畫出方法),補充一下關於 os.File 結構體的圖:
這一節的最後,複習一下上一篇關於 interface 的文章,提到的一個技巧,這裡再展示一下:
先參考原始碼,分別定義一個 “偽裝”
的 iface 和 eface 結構體。
-
type iface struct {
-
tab *itab
-
data unsafe.Pointer
-
}
-
type itab struct {
-
inter uintptr
-
_type uintptr
-
link uintptr
-
hash uint32
-
_ [4]byte
-
fun [1]uintptr
-
}
-
-
type eface struct {
-
_type uintptr
-
data unsafe.Pointer
-
}
接著,將介面變數佔據的記憶體內容強制解釋成上面定義的型別,再打印出來:
-
package main
-
-
import (
-
"os"
-
"fmt"
-
"io"
-
"unsafe"
-
)
-
-
func main() {
-
var r io.Reader
-
fmt.Printf("initial r: %T, %v\n", r, r)
-
-
tty, _ := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)
-
fmt.Printf("tty: %T, %v\n", tty, tty)
-
-
// 給 r 賦值
-
r = tty
-
fmt.Printf("r: %T, %v\n", r, r)
-
-
rIface := (*iface)(unsafe.Pointer(&r;))
-
fmt.Printf("r: iface.tab._type = %#x, iface.data = %#x\n", rIface.tab._type, rIface.data)
-
-
// 給 w 賦值
-
var w io.Writer
-
w = r.(io.Writer)
-
fmt.Printf("w: %T, %v\n", w, w)
-
-
wIface := (*iface)(unsafe.Pointer(&w;))
-
fmt.Printf("w: iface.tab._type = %#x, iface.data = %#x\n", wIface.tab._type, wIface.data)
-
-
// 給 empty 賦值
-
var empty interface{}
-
empty = w
-
fmt.Printf("empty: %T, %v\n", empty, empty)
-
-
emptyEface := (*eface)(unsafe.Pointer(∅))
-
fmt.Printf("empty: eface._type = %#x, eface.data = %#x\n", emptyEface._type, emptyEface.data)
-
}
執行結果:
-
initial r: ,
-
tty: *os.File, &{0xc4200820f0}
-
r: *os.File, &{0xc4200820f0}
-
r: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020
-
w: *os.File, &{0xc4200820f0}
-
w: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020
-
empty: *os.File, &{0xc4200820f0}
-
empty: eface._type = 0x10bfcc0, eface.data = 0xc420080020
r,w,empty
的動態型別和動態值都一樣。不再詳細解釋了,結合前面的圖可以看得非常清晰。
反射的基本函式
reflect 包裡定義了一個介面和一個結構體,即 reflect.Type
和 reflect.Value
,它們提供很多函式來獲取儲存在介面裡的型別資訊。
reflect.Type
主要提供關於型別相關的資訊,所以它和 _type
關聯比較緊密; reflect.Value
則結合 _type
和 data
兩者,因此程式員可以獲取甚至改變型別的值。
reflect 包中提供了兩個基礎的關於反射的函式來獲取上述的介面和結構體:
-
func TypeOf(i interface{}) Type
-
func ValueOf(i interface{}) Value
TypeOf
函式用來提取一個介面中值的型別資訊。由於它的輸入引數是一個空的 interface{}
,呼叫此函式時,引數會先被轉化為 interface{}
型別。這樣,引數的型別資訊、方法集、值資訊都儲存到 interface{}
變數裡了。
看下原始碼:
-
func TypeOf(i interface{}) Type {
-
eface := *(*emptyInterface)(unsafe.Pointer(&i;))
-
return toType(eface.typ)
-
}
這裡的 emptyInterface
和上面提到的 eface
是一回事(欄位名略有差異,欄位是相同的),且在不同的原始碼包:前者在 reflect
包,後者在 runtime
包。 eface.typ
就是動態型別。
-
type emptyInterface struct {
-
typ *rtype
-
word unsafe.Pointer
-
}
至於 toType
函式,只是做了一個型別轉換:
-
func toType(t *rtype) Type {
-
if t == nil {
-
return nil
-
}
-
return t
-
}
註意,傳回值 Type
實際上是一個介面,定義了很多方法,用來獲取型別相關的各種資訊,而 *rtype
實現了 Type
介面。
-
type Type interface {
-
// 所有的型別都可以呼叫下麵這些函式
-
-
// 此型別的變數對齊後所佔用的位元組數
-
Align() int
-
-
// 如果是 struct 的欄位,對齊後佔用的位元組數
-
FieldAlign() int
-
-
// 傳回型別方法集裡的第 `i` (傳入的引數)個方法
-
Method(int) Method
-
-
// 透過名稱獲取方法
-
MethodByName(string) (Method, bool)
-
-
// 獲取型別方法集裡匯出的方法個數
-
NumMethod() int
-
-
// 型別名稱
-
Name() string
-
-
// 傳回型別所在的路徑,如:encoding/base64
-
PkgPath() string
-
-
// 傳回型別的大小,和 unsafe.Sizeof 功能類似
-
Size() uintptr
-
-
// 傳回型別的字串表示形式
-
String() string
-
-
// 傳回型別的型別值
-
Kind() Kind
-
-
// 型別是否實現了介面 u
-
Implements(u Type) bool
-
-
// 是否可以賦值給 u
-
AssignableTo(u Type) bool
-
-
// 是否可以型別轉換成 u
-
ConvertibleTo(u Type) bool
-
-
// 型別是否可以比較
-
Comparable() bool
-
-
// 下麵這些函式只有特定型別可以呼叫
-
// 如:Key, Elem 兩個方法就只能是 Map 型別才能呼叫
-
-
// 型別所佔據的位數
-
Bits() int
-
-
// 傳回通道的方向,只能是 chan 型別呼叫
-
ChanDir() ChanDir
-
-
// 傳回型別是否是可變引數,只能是 func 型別呼叫
-
// 比如 t 是型別 func(x int, y ... float64)
-
// 那麼 t.IsVariadic() == true
-
IsVariadic() bool
-
-
// 傳回內部子元素型別,只能由型別 Array, Chan, Map, Ptr, or Slice 呼叫
-
Elem() Type
-
-
// 傳回結構體型別的第 i 個欄位,只能是結構體型別呼叫
-
// 如果 i 超過了總欄位數,就會 panic
-
Field(i int) StructField
-
-
// 傳回巢狀的結構體的欄位
-
FieldByIndex(index []int) StructField
-
-
// 透過欄位名稱獲取欄位
-
FieldByName(name string) (StructField, bool)
-
-
// FieldByNameFunc returns the struct field with a name
-
// 傳回名稱符合 func 函式的欄位
-
FieldByNameFunc(match func(string) bool) (StructField, bool)
-
-
// 獲取函式型別的第 i 個引數的型別
-
In(i int) Type
-
-
// 傳回 map 的 key 型別,只能由型別 map 呼叫
-
Key() Type
-
-
// 傳回 Array 的長度,只能由型別 Array 呼叫
-
Len() int
-
-
// 傳回型別欄位的數量,只能由型別 Struct 呼叫
-
NumField() int
-
-
// 傳回函式型別的輸入引數個數
-
NumIn() int
-
-
// 傳回函式型別的傳回值個數
-
NumOut() int
-
-
// 傳回函式型別的第 i 個值的型別
-
Out(i int) Type
-
-
// 傳回型別結構體的相同部分
-
common() *rtype
-
-
// 傳回型別結構體的不同部分
-
uncommon() *uncommonType
-
}
可見 Type
定義了非常多的方法,透過它們可以獲取型別的一切資訊,大家一定要完整的過一遍上面所有的方法。
註意到 Type
方法集的倒數第二個方法 common
傳回的 rtype
型別,它和上一篇文章講到的 _type
是一回事,而且原始碼裡也註釋了:兩邊要保持同步:
-
// 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
-
alg *typeAlg
-
gcdata *byte
-
str nameOff
-
ptrToThis typeOff
-
}
所有的型別都會包含 rtype
這個欄位,表示各種型別的公共資訊;另外,不同型別包含自己的一些獨特的部分。
比如下麵的 arrayType
和 chanType
都包含 rytpe
,而前者還包含 slice,len 等和陣列相關的資訊;後者則包含 dir
表示通道方向的資訊。
-
// arrayType represents a fixed array type.
-
type arrayType struct {
-
rtype `reflect:"array"`
-
elem *rtype // array element type
-
slice *rtype // slice type
-
len uintptr
-
}
-
-
// chanType represents a channel type.
-
type chanType struct {
-
rtype `reflect:"chan"`
-
elem *rtype // channel element type
-
dir uintptr // channel direction (ChanDir)
-
}
註意到, Type
介面實現了 String()
函式,滿足 fmt.Stringer
介面,因此使用 fmt.Println
列印的時候,輸出的是 String()
的結果。另外, fmt.Printf()
函式,如果使用 %T
來作為格式引數,輸出的是 reflect.TypeOf
的結果,也就是動態型別。例如:
-
fmt.Printf("%T", 3) // int
講完了 TypeOf
函式,再來看一下 ValueOf
函式。傳回值 reflect.Value
表示 interface{}
裡儲存的實際變數,它能提供實際變數的各種資訊。相關的方法常常是需要結合型別資訊和值資訊。例如,如果要提取一個結構體的欄位資訊,那就需要用到 _type (具體到這裡是指 structType) 型別持有的關於結構體的欄位資訊、偏移資訊,以及 *data
所指向的內容 —— 結構體的實際值。
原始碼如下:
-
func ValueOf(i interface{}) Value {
-
if i == nil {
-
return Value{}
-
}
-
-
// ……
-
return unpackEface(i)
-
}
-
-
// 分解 eface
-
func unpackEface(i interface{}) Value {
-
e := (*emptyInterface)(unsafe.Pointer(&i;))
-
-
t := e.typ
-
if t == nil {
-
return Value{}
-
}
-
-
f := flag(t.Kind())
-
if ifaceIndir(t) {
-
f |= flagIndir
-
}
-
return Value{t, e.word, f}
-
}
從原始碼看,比較簡單:將先將 i
轉換成 *emptyInterface
型別, 再將它的 typ
欄位和 word
欄位以及一個標誌位欄位組裝成一個 Value
結構體,而這就是 ValueOf
函式的傳回值,它包含型別結構體指標、真實資料的地址、標誌位。
Value 結構體定義了很多方法,透過這些方法可以直接操作 Value 欄位 ptr 所指向的實際資料:
-
// 設定切片的 len 欄位,如果型別不是切片,就會panic
-
func (v Value) SetLen(n int)
-
-
// 設定切片的 cap 欄位
-
func (v Value) SetCap(n int)
-
-
// 設定字典的 kv
-
func (v Value) SetMapIndex(key, val Value)
-
-
// 傳回切片、字串、陣列的索引 i 處的值
-
func (v Value) Index(i int) Value
-
-
// 根據名稱獲取結構體的內部欄位值
-
func (v Value) FieldByName(name string) Value
-
-
// ……
Value
欄位還有很多其他的方法。例如:
-
// 用來獲取 int 型別的值
-
func (v Value) Int() int64
-
-
// 用來獲取結構體欄位(成員)數量
-
func (v Value) NumField() int
-
-
// 嘗試向通道傳送資料(不會阻塞)
-
func (v Value) TrySend(x reflect.Value) bool
-
-
// 透過引數串列 in 呼叫 v 值所代表的函式(或方法
-
func (v Value) Call(in []Value) (r []Value)
-
-
// 呼叫變參長度可變的函式
-
func (v Value) CallSlice(in []Value) []Value
不一一列舉了,反正是非常多。可以去 src/reflect/value.go
去看看原始碼,搜尋 func(vValue)
就能看到。
另外,透過 Type()
方法和 Interface()
方法可以打通 interface
、 Type
、 Value
三者。Type() 方法也可以傳回變數的型別資訊,與 reflect.TypeOf() 函式等價。Interface() 方法可以將 Value 還原成原來的 interface。
這裡取用老錢《快學Go語言第十五課——反射》的一張圖:
總結一下: TypeOf()
函式傳回一個介面,這個介面定義了一系列方法,利用這些方法可以獲取關於型別的所有資訊; ValueOf()
函式傳回一個結構體變數,包含型別資訊以及實際值。
用一張圖來串一下:
上圖中, rtye
實現了 Type
介面,是所有型別的公共部分。emptyface 結構體和 eface 其實是一個東西,而 rtype 其實和 _type 是一個東西,只是一些欄位稍微有點差別,比如 emptyface 的 word 欄位和 eface 的 data 欄位名稱不同,但是資料型是一樣的。
反射的三大定律
根據 Go 官方關於反射的部落格,反射有三大定律:
Reflection goes from interface value to reflection object.
Reflection goes from reflection object to interface value.
To modify a reflection object, the value must be settable.
第一條是最基本的:反射是一種檢測儲存在 interface
中的型別和值機制。這可以透過 TypeOf
函式和 ValueOf
函式得到。
第二條實際上和第一條是相反的機制,它將 ValueOf
的傳回值透過 Interface()
函式反向轉變成 interface
變數。
前兩條就是說 介面型變數
和 反射型別物件
可以相互轉化,反射型別物件實際上就是指的前面說的 reflect.Type
和 reflect.Value
。
第三條不太好懂:如果需要操作一個反射變數,那麼它必須是可設定的。反射變數可設定的本質是它儲存了原變數本身,這樣對反射變數的操作,就會反映到原變數本身;反之,如果反射變數不能代表原變數,那麼操作了反射變數,不會對原變數產生任何影響,這會給使用者帶來疑惑。所以第二種情況在語言層面是不被允許的。
舉一個經典例子:
-
var x float64 = 3.4
-
v := reflect.ValueOf(x)
-
v.SetFloat(7.1) // Error: will panic.
執行上面的程式碼會產生 panic,原因是反射變數 v
不能代表 x
本身,為什麼?因為呼叫 reflect.ValueOf(x)
這一行程式碼的時候,傳入的引數在函式內部只是一個複製,是值傳遞,所以 v
代表的只是 x
的一個複製,因此對 v
進行操作是被禁止的。
可設定是反射變數 Value
的一個性質,但不是所有的 Value
都是可被設定的。
就像在一般的函式裡那樣,當我們想改變傳入的變數時,使用指標就可以解決了。
-
var x float64 = 3.4
-
p := reflect.ValueOf(&x;)
-
fmt.Println("type of p:", p.Type())
-
fmt.Println("settability of p:", p.CanSet())
輸出是這樣的:
-
type of p: *float64
-
settability of p: false
p
還不是代表 x
, p.Elem()
才真正代表 x
,這樣就可以真正操作 x
了:
-
v := p.Elem()
-
v.SetFloat(7.1)
-
fmt.Println(v.Interface()) // 7.1
-
fmt.Println(x) // 7.1
關於第三條,記住一句話:如果想要操作原變數,反射變數 Value
必須要 hold 住原變數的地址才行。
反射相關函式的使用
程式碼樣例
網路上各種部落格文章裡使用反射的樣例程式碼非常多,讀過這篇文章後,基本沒有看不懂的,哈哈!不過,我這裡還是舉一個例子,並講解一番:
-
package main
-
-
import (
-
"reflect"
-
"fmt"
-
)
-
-
type Child struct {
-
Name string
-
Grade int
-
Handsome bool
-
}
-
-
type Adult struct {
-
ID string `qson:"Name"`
-
Occupation string
-
Handsome bool
-
}
-
-
// 如果輸入引數 i 是 Slice,元素是結構體,有一個欄位名為 `Handsome`,
-
// 並且有一個欄位的 tag 或者欄位名是 `Name` ,
-
// 如果該 `Name` 欄位的值是 `qcrao`,
-
// 就把結構體中名為 `Handsome` 的欄位值設定為 true。
-
func handsome(i interface{}) {
-
// 獲取 i 的反射變數 Value
-
v := reflect.ValueOf(i)
-
-
// 確定 v 是一個 Slice
-
if v.Kind() != reflect.Slice {
-
return
-
}
-
-
// 確定 v 是的元素為結構體
-
if e := v.Type().Elem(); e.Kind() != reflect.Struct {
-
return
-
}
-
-
// 確定結構體的欄位名含有 "ID" 或者 json tag 標簽為 `name`
-
// 確定結構體的欄位名 "Handsome"
-
st := v.Type().Elem()
-
-
// 尋找欄位名為 Name 或者 tag 的值為 Name 的欄位
-
foundName := false
-
for i := 0; i < st.NumField(); i++ {
-
f := st.Field(i)
-
tag := f.Tag.Get("qson")
-
-
if (tag == "Name" || f.Name == "Name") && f.Type.Kind() == reflect.String {
-
foundName = true
-
break
-
}
-
}
-
-
if !foundName {
-
return
-
}
-
-
if niceField, foundHandsome := st.FieldByName("Handsome"); foundHandsome == false || niceField.Type.Kind() != reflect.Bool {
-
return
-
}
-
-
// 設定名字為 "qcrao" 的物件的 "Handsome" 欄位為 true
-
for i := 0; i < v.Len(); i++ {
-
e := v.Index(i)
-
handsome := e.FieldByName("Handsome")
-
-
// 尋找欄位名為 Name 或者 tag 的值為 Name 的欄位
-
var name reflect.Value
-
for j := 0; j < st.NumField(); j++ {
-
f := st.Field(j)
-
tag := f.Tag.Get("qson")
-
-
if tag == "Name" || f.Name == "Name" {
-
name = v.Index(i).Field(j)
-
}
-
}
-
-
if name.String() == "qcrao" {
-
handsome.SetBool(true)
-
}
-
}
-
}
-
-
func main() {
-
children := []Child{
-
{Name: "Ava", Grade: 3, Handsome: true},
-
{Name: "qcrao", Grade: 6, Handsome: false},
-
}
-
-
adults := []Adult{
-
{ID: "Steve", Occupation: "Clerk", Handsome: true},
-
{ID: "qcrao", Occupation: "Go Programmer", Handsome: false},
-
}
-
-
fmt.Printf("adults before handsome: %v\n", adults)
-
handsome(adults)
-
fmt.Printf("adults after handsome: %v\n", adults)
-
-
fmt.Println("-------------")
-
-
fmt.Printf("children before handsome: %v\n", children)
-
handsome(children)
-
fmt.Printf("children after handsome: %v\n", children)
-
}
程式碼執行結果:
-
adults before handsome: [{Steve Clerk true} {qcrao Go Programmer false}]
-
adults after handsome: [{Steve Clerk true} {qcrao Go Programmer true}]
-
-------------
-
children before handsome: [{Ava 3 true} {qcrao 6 false}]
-
children after handsome: [{Ava 3 true} {qcrao 6 true}]
程式碼主要做的事情是:找出傳入的引數為 Slice,並且 Slice 的元素為結構體,如果其中有一個欄位名是 Name
或者是 標簽名稱為 Name
,並且還有一個欄位名是 Handsome
的情形。如果找到,並且欄位名稱為 Name
的實際值是 qcrao
的話,就把另一個欄位 Handsome
的值置為 true。
程式並不關心傳入的結構體到底是什麼,只要它的欄位名包含 Name
和 Handsome
,都是 handsome 函式要工作的物件。
註意一點, Adult
結構體的標簽 qson:"Name"
,中間是沒有空格的,否則 Tag.Get("qson")
識別不出來。
未匯出成員
利用反射機制,對於結構體中未匯出成員,可以讀取,但不能修改其值。
註意,正常情況下,程式碼是不能讀取結構體未匯出成員的,但透過反射可以越過這層限制。另外,透過反射,結構體中可以被修改的成員只有是匯出成員,也就是欄位名的首字母是大寫的。
一個可取地址的 reflect.Value 變數會記錄一個結構體成員是否是未匯出成員,如果是的話則拒絕修改操作。
CanAddr 不能說明一個變數是否可以被修改。
CanSet 則可以檢查對應的 reflect.Value 是否可取地址並可被修改。
-
package main
-
-
import (
-
"reflect"
-
"fmt"
-
)
-
-
type Child struct {
-
Name string
-
handsome bool
-
}
-
-
func main() {
-
qcrao := Child{Name: "qcrao", handsome: true}
-
-
v := reflect.ValueOf(&qcrao;)
-
-
f := v.Elem().FieldByName("Name")
-
fmt.Println(f.String())
-
-
f.SetString("stefno")
-
fmt.Println(f.String())
-
-
f = v.Elem().FieldByName("handsome")
-
-
// 這一句會導致 panic,因為 handsome 欄位未匯出
-
//f.SetBool(true)
-
fmt.Println(f.Bool())
-
}
執行結果:
-
qcrao
-
stefno
-
true
上面的例子中,handsome 欄位未匯出,可以讀取,但不能呼叫相關 set 方法,否則會 panic。反射用起來一定要小心,呼叫型別不匹配的方法,會導致各種 panic。
反射的實際應用
反射的實際應用非常廣:IDE 中的程式碼自動補全功能、物件序列化(json 函式庫)、fmt 相關函式的實現、ORM(全稱是:Object Relational Mapping,物件關係對映)……
這裡舉 2 個例子:json 序列化和 DeepEqual 函式。
json 序列化
開發過 web 服務的同學,一定用過 json
資料格式。 json
是一種獨立於語言的資料格式。最早用於瀏覽器和伺服器之間的實時無狀態的資料交換,並由此發展起來。
Go 語言中,主要提供 2 個函式用於序列化和反序列化:
-
func Marshal(v interface{}) ([]byte, error)
-
func Unmarshal(data []byte, v interface{}) error
兩個函式的引數都包含 interface
,具體實現的時候,都會用到反射相關的特性。
對於序列化和反序列化函式,均需要知道引數的所有欄位,包括欄位型別和值,再呼叫相關的 get 函式或者 set 函式進行實際的操作。
DeepEqual 的作用及原理
在測試函式中,經常會需要這樣的函式:判斷兩個變數的實際內容完全一致。
例如:如何判斷兩個 slice 所有的元素完全相同;如何判斷兩個 map 的 key 和 value 完全相同等等。
上述問題,可以透過 DeepEqual
函式實現。
-
func DeepEqual(x, y interface{}) bool
DeepEqual
函式的引數是兩個 interface
,實際上也就是可以輸入任意型別,輸出 true 或者 flase 表示輸入的兩個變數是否是“深度”相等。
先明白一點,如果是不同的型別,即使是底層型別相同,相應的值也相同,那麼兩者也不是“深度”相等。
-
type MyInt int
-
type YourInt int
-
-
func main() {
-
m := MyInt(1)
-
y := YourInt(1)
-
-
fmt.Println(reflect.DeepEqual(m, y)) // false
-
}
上面的程式碼中,m, y 底層都是 int,而且值都是 1,但是兩者靜態型別不同,前者是 MyInt
,後者是 YourInt
,因此兩者不是“深度”相等。
在原始碼裡,有對 DeepEqual 函式的非常清楚地註釋,列舉了不同型別,DeepEqual 的比較情形,這裡做一個總結:
型別 | 深度相等情形 |
---|---|
Array | 相同索引處的元素“深度”相等 |
Struct | 相應欄位,包含匯出和不匯出,“深度”相等 |
Func | 只有兩者都是 nil 時 |
Interface | 兩者儲存的具體值“深度”相等 |
Map | 1、都為 nil;2、非空、長度相等,指向同一個 map 物體物件,或者相應的 key 指向的 value “深度”相等 |
Pointer | 1、使用 == 比較的結果相等;2、指向的物體“深度”相等 |
Slice | 1、都為 nil;2、非空、長度相等,首元素指向同一個底層陣列的相同元素,即 &x;[0] == &y;[0] 或者 相同索引處的元素“深度”相等 |
numbers, bools, strings, and channels | 使用 == 比較的結果為真 |
一般情況下,DeepEqual 的實現只需要遞迴地呼叫 == 就可以比較兩個變數是否是真的“深度”相等。
但是,有一些異常情況:比如 func 型別是不可比較的型別,只有在兩個 func 型別都是 nil 的情況下,才是“深度”相等;float 型別,由於精度的原因,也是不能使用 == 比較的;包含 func 型別或者 float 型別的 struct, interface, array 等。
對於指標而言,當兩個值相等的指標就是“深度”相等,因為兩者指向的內容是相等的,即使兩者指向的是 func 型別或者 float 型別,這種情況下不關心指標所指向的內容。
同樣,對於指向相同 slice, map 的兩個變數也是“深度”相等的,不關心 slice, map 具體的內容。
對於“有環”的型別,比如迴圈連結串列,比較兩者是否“深度”相等的過程中,需要對已比較的內容作一個標記,一旦發現兩個指標之前比較過,立即停止比較,並判定二者是深度相等的。這樣做的原因是,及時停止比較,避免陷入無限迴圈。
來看原始碼:
-
func DeepEqual(x, y interface{}) bool {
-
if x == nil || y == nil {
-
return x == y
-
}
-
v1 := ValueOf(x)
-
v2 := ValueOf(y)
-
if v1.Type() != v2.Type() {
-
return false
-
}
-
return deepValueEqual(v1, v2, make(map[visit]bool), 0)
-
}
首先檢視兩者是否有一個是 nil 的情況,這種情況下,只有兩者都是 nil,函式才會傳回 true。
接著,使用反射,獲取x,y 的反射物件,並且立即比較兩者的型別,根據前面的內容,這裡實際上是動態型別,如果型別不同,直接傳回 false。
最後,最核心的內容在子函式 deepValueEqual
中。
程式碼比較長,思路卻比較簡單清晰:核心是一個 switch 陳述句,識別輸入引數的不同型別,分別遞迴呼叫 deepValueEqual 函式,一直遞迴到最基本的資料型別,比較 int,string 等可以直接得出 true 或者 false,再一層層地傳回,最終得到“深度”相等的比較結果。
實際上,各種型別的比較套路比較相似,這裡就直接節選一個稍微複雜一點的 map
型別的比較:
-
// deepValueEqual 函式
-
// ……
-
-
case Map:
-
if v1.IsNil() != v2.IsNil() {
-
return false
-
}
-
if v1.Len() != v2.Len() {
-
return false
-
}
-
if v1.Pointer() == v2.Pointer() {
-
return true
-
}
-
for _, k := range v1.MapKeys() {
-
val1 := v1.MapIndex(k)
-
val2 := v2.MapIndex(k)
-
if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) {
-
return false
-
}
-
}
-
return true
-
-
// ……
和前文總結的表格裡,比較 map 是否相等的思路比較一致,也不需要多說什麼。說明一點, visited
是一個 map,記錄遞迴過程中,比較過的“對”:
-
type visit struct {
-
a1 unsafe.Pointer
-
a2 unsafe.Pointer
-
typ Type
-
}
-
-
map[visit]bool
比較過程中,一旦發現比較的“對”,已經在 map 裡出現過的話,直接判定“深度”比較結果的是 true
。
總結
Go 作為一門靜態語言,相比 Python 等動態語言,在編寫過程中靈活性會受到一定的限制。但是透過介面加反射實現了類似於動態語言的能力:可以在程式執行時動態地捕獲甚至改變型別的資訊和值。
Go 語言的反射實現的基礎是型別,或者說是 interface,當我們使用反射特性時,實際上用到的就是儲存在 interface 變數中的和型別相關的資訊,也就是常說的 對。
只有 interface 才有反射的說法。
反射在 reflect 包中實現,涉及到兩個相關函式:
-
func TypeOf ( i interface{} ) Type
-
func ValueOf ( i interface{} ) Value
Type 是一個介面,定義了很多相關方法,用於獲取型別資訊。Value 則持有型別的具體值。Type、Value、Interface 三者間透過函式 TypeOf,ValueOf,Interface 進行相互轉換。
最後溫習一下反射三大定律:
Reflection goes from interface value to reflection object.
Reflection goes from reflection object to interface value.
To modify a reflection object, the value must be settable.
翻譯一下:
反射將介面變數轉換成反射物件 Type 和 Value;
反射可以透過反射物件 Value 還原成原先的介面變數;
反射可以用來修改一個變數的值,前提是這個值可以被修改。
參考資料
【維基百科中文】https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%B0%84_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)
【碼洞老錢 反射】https://juejin.im/post/5c2040d76fb9a049c643d9bd
【Go官方部落格 reflection】https://blog.golang.org/laws-of-reflection
【GCTT譯文,不錯】https://mp.weixin.qq.com/s/dkgJ_fA0smvpv69t5Nv-7A
【json庫 原始碼分析】https://zhuanlan.zhihu.com/p/37165706
【reflect 程式碼例子和圖比較好】https://blog.gopheracademy.com/advent-2018/interfaces-and-reflect/
【反射使用講得不錯】https://juejin.im/post/5a75a4fb5188257a82110544
【介面和反射的關係 ,english】https://blog.gopheracademy.com/advent-2018/interfaces-and-reflect/
【總結成知識點】http://www.cnblogs.com/susufufu/p/7653579.html
【Type Value】https://colobu.com/2016/07/09/dive-into-go-13/
【講得比較清晰簡單】https://www.lijiaocn.com/%E7%BC%96%E7%A8%8B/2017/11/06/golang-reflection.html
【DeepEqual】https://github.com/Chasiny/Blog/blob/master/blog/go/package/go-reflect-deepequal.md
【反射使用場景】https://yq.aliyun.com/articles/599584
朋友會在“發現-看一看”看到你“在看”的內容