反射和 Interface 息息相關,而 Interface 是我們上一篇文章的內容。在開始正文前,和大家說點題外話。
上一篇關于 Interface 的文章發出后,獲得了很多的關注和閱讀。比如,登上了 GoCN 的每日新聞第一條:
可能是編輯者覺得這篇文章稱不上“深度解密”,把標題給小小地改動了下,哈哈~~
在博客園登上了 48 小時閱讀排行榜:
在開發者頭條 APP (類似于今日頭條,不過內容都是技術相關的,還挺有意思的)上收獲了 150 收藏,并被推薦到首頁最顯眼的 banner 位置,閱讀量達到了 1w 多,只是不知道這個數字是否是真實的,有點難以相信。
很多同學在后臺向我反映文章太長了,不利于閱讀,建議拆分一下。我非常理解,讀屏時代,大家需要快速地讀完全文,拿到收益。而碼農桃花源的文章都非常長,讀者很難在短時間內讀完,并且獲得相應的收益。
首先非常感謝大家的建議!其實我的想法是這樣的:大家都在說,現在是一個信息嚴重過載的時代,信息多得看不完,不免產生很多焦慮。但是,我想說,優質的信息真有那么多嗎?在我看來,文章的水平都是參差不齊,很多毫無內容和價值,大家把時間浪費在這些信息上面是很不值得的。因為你讀了這些文章,就沒有了讀其他好的文章的精力。
所以,碼農桃花源想做一個優質信息源,提供優質的內容。每一篇文章都是有深度,有內容,有收獲。一篇文章,我一般得花費 2 周左右,算是半月更,和那些日更的沒法比。當然,只是在數量上沒法比。而這個時代,最不缺的就是數量。
另外,文章長也算是我的一個特色。我完全可以拆分成上、中、下等等,但我希望一次性交付給我的讀者所有有價值的內容。這樣,你可以集中一個小時或是更長時間,精讀完一篇文章。
微信公眾號閱讀文章有時不是太方便,這里推薦大家用微信讀書,直接搜索公眾號名字就可以看到公眾號所有的文章了,很方便。這個功能出來很久了,我也是不久前才知道,推薦給大家。
閑話結束,今天要講的內容是反射,進入正題。
什么是反射
直接看維基百科上的定義:
在計算機科學中,反射是指計算機程序在運行時(Run time)可以訪問、檢測和修改它本身狀態或行為的一種能力。用比喻來說,反射就是程序在運行的時候能夠“觀察”并且修改自己的行為。
那我就要問個問題了:不用反射就不能在運行時訪問、檢測和修改它本身的狀態和行為嗎?
問題的回答,其實要首先理解什么叫訪問、檢測和修改它本身狀態或行為,它的本質是什么?
實際上,它的本質是程序在運行期探知對象的類型信息和內存結構,不用反射能行嗎?可以的!使用匯編語言,直接和內層打交道,什么信息不能獲取?但是,當編程遷移到高級語言上來之后,就不行了!就只能通過反射
來達到此項技能。
不同語言的反射模型不盡相同,有些語言還不支持反射。《Go 語言圣經》中是這樣定義反射的:
Go 語言提供了一種機制在運行時更新變量和檢查它們的值、調用它們的方法,但是在編譯時并不知道這些變量的具體類型,這稱為反射機制。
為什么要用反射
需要反射的 2 個常見場景:
- 有時你需要編寫一個函數,但是并不知道傳給你的參數類型是什么,可能是沒約定好;也可能是傳入的類型很多,這些類型并不能統一表示。這時反射就會用的上了。
- 有時候需要根據某些條件決定調用哪個函數,比如根據用戶的輸入來決定。這時就需要對函數和函數的參數進行反射,在運行期間動態地執行函數。
在講反射的原理以及如何用之前,還是說幾點不使用反射的理由:
- 與反射相關的代碼,經常是難以閱讀的。在軟件工程中,代碼可讀性也是一個非常重要的指標。
- Go 語言作為一門靜態語言,編碼過程中,編譯器能提前發現一些類型錯誤,但是對于反射代碼是無能為力的。所以包含反射相關的代碼,很可能會運行很久,才會出錯,這時候經常是直接 panic,可能會造成嚴重的后果。
- 反射對性能影響還是比較大的,比正常代碼運行速度慢一到兩個數量級。所以,對于一個項目中處于運行效率關鍵位置的代碼,盡量避免使用反射特性。
反射是如何實現的
上一篇文章講到了 interface
,它是 Go 語言實現抽象的一個非常強大的工具。當向接口變量賦予一個實體類型的時候,接口會存儲實體的類型信息,反射就是通過接口的類型信息實現的,反射建立在類型的基礎上。
Go 語言在 reflect 包里定義了各種類型,實現了反射的各種函數,通過它們可以在運行時檢測類型的信息、改變類型的值。
types 和 interface
Go 語言中,每個變量都有一個靜態類型,在編譯階段就確定了的,比如 int, float64, []int
等等。注意,這個類型是聲明時候的類型,不是底層數據類型。
Go 官方博客里就舉了一個例子:
type MyInt intvar i int
var j MyInt
復制代碼
盡管 i,j 的底層類型都是 int,但我們知道,他們是不同的靜態類型,除非進行類型轉換,否則,i 和 j 不能同時出現在等號兩側。j 的靜態類型就是 MyInt
。
反射主要與 interface{} 類型相關。前面一篇關于 interface 相關的文章已經探討過 interface 的底層結構,這里再來復習一下。
type iface struct {tab *itabdata unsafe.Pointer
}type itab struct {inter *interfacetype_type *_typelink *itabhash uint32bad boolinhash boolunused [2]bytefun [1]uintptr
}
復制代碼
其中 itab
由具體類型 _type
以及 interfacetype
組成。_type
表示具體類型,而 interfacetype
則表示具體類型實現的接口類型。
實際上,iface 描述的是非空接口,它包含方法;與之相對的是 eface
,描述的是空接口,不包含任何方法,Go 語言里有的類型都 “實現了”
空接口。
type eface struct {_type *_typedata 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 可以用<value, type>
對來表示為: <tty, *os.File>
。
注意看上圖,此時雖然 fun
所指向的函數只有一個 Read
函數,其實 *os.File
還包含 Write
函數,也就是說 *os.File
其實還實現了 io.Writer
接口。因此下面的斷言語句可以執行:
var w io.Writer
w = r.(io.Writer)
復制代碼
之所以用斷言,而不能直接賦值,是因為 r
的靜態類型是 io.Reader
,并沒有實現 io.Writer
接口。斷言能否成功,看 r
的動態類型是否符合要求。
這樣,w 也可以表示成 <tty, *os.File>
,僅管它和 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 *itabdata unsafe.Pointer
}
type itab struct {inter uintptr_type uintptrlink uintptrhash uint32_ [4]bytefun [1]uintptr
}type eface struct {_type uintptrdata unsafe.Pointer
}
復制代碼
接著,將接口變量占據的內存內容強制解釋成上面定義的類型,再打印出來:
package mainimport ("os""fmt""io""unsafe"
)func main() {var r io.Readerfmt.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 = ttyfmt.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.Writerw = 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 = wfmt.Printf("empty: %T, %v\n", empty, empty)emptyEface := (*eface)(unsafe.Pointer(&empty))fmt.Printf("empty: eface._type = %#x, eface.data = %#x\n", emptyEface._type, emptyEface.data)
}
復制代碼
運行結果:
initial r: <nil>, <nil>
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 *rtypeword 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/base64PkgPath() string// 返回類型的大小,和 unsafe.Sizeof 功能類似Size() uintptr// 返回類型的字符串表示形式String() string// 返回類型的類型值Kind() Kind// 類型是否實現了接口 uImplements(u Type) bool// 是否可以賦值給 uAssignableTo(u Type) bool// 是否可以類型轉換成 uConvertibleTo(u Type) bool// 類型是否可以比較Comparable() bool// 下面這些函數只有特定類型可以調用// 如:Key, Elem 兩個方法就只能是 Map 類型才能調用// 類型所占據的位數Bits() int// 返回通道的方向,只能是 chan 類型調用ChanDir() ChanDir// 返回類型是否是可變參數,只能是 func 類型調用// 比如 t 是類型 func(x int, y ... float64)// 那么 t.IsVariadic() == trueIsVariadic() bool// 返回內部子元素類型,只能由類型 Array, Chan, Map, Ptr, or Slice 調用Elem() Type// 返回結構體類型的第 i 個字段,只能是結構體類型調用// 如果 i 超過了總字段數,就會 panicField(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 uintptrptrdata uintptrhash uint32tflag tflagalign uint8fieldAlign uint8kind uint8alg *typeAlggcdata *bytestr nameOffptrToThis typeOff
}
復制代碼
所有的類型都會包含 rtype
這個字段,表示各種類型的公共信息;另外,不同類型包含自己的一些獨特的部分。
比如下面的 arrayType
和 chanType
都包含 rytpe
,而前者還包含 slice,len 等和數組相關的信息;后者則包含 dir
表示通道方向的信息。
// arrayType represents a fixed array type.
type arrayType struct {rtype `reflect:"array"`elem *rtype // array element typeslice *rtype // slice typelen uintptr
}// chanType represents a channel type.
type chanType struct {rtype `reflect:"chan"`elem *rtype // channel element typedir 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.typif 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 字段,如果類型不是切片,就會panicfunc (v Value) SetLen(n int)// 設置切片的 cap 字段func (v Value) SetCap(n int)// 設置字典的 kvfunc (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 (v Value)
就能看到。
另外,通過 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 mainimport ("reflect""fmt"
)type Child struct {Name stringGrade intHandsome bool
}type Adult struct {ID string `qson:"Name"`Occupation stringHandsome bool
}// 如果輸入參數 i 是 Slice,元素是結構體,有一個字段名為 `Handsome`,
// 并且有一個字段的 tag 或者字段名是 `Name` ,
// 如果該 `Name` 字段的值是 `qcrao`,
// 就把結構體中名為 `Handsome` 的字段值設置為 true。
func handsome(i interface{}) {// 獲取 i 的反射變量 Valuev := reflect.ValueOf(i)// 確定 v 是一個 Sliceif 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 := falsefor 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 = truebreak}}if !foundName {return}if niceField, foundHandsome := st.FieldByName("Handsome"); foundHandsome == false || niceField.Type.Kind() != reflect.Bool {return}// 設置名字為 "qcrao" 的對象的 "Handsome" 字段為 truefor i := 0; i < v.Len(); i++ {e := v.Index(i)handsome := e.FieldByName("Handsome")// 尋找字段名為 Name 或者 tag 的值為 Name 的字段var name reflect.Valuefor 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 mainimport ("reflect""fmt"
)type Child struct {Name stringhandsome 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 intfunc 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.Pointera2 unsafe.Pointertyp Type
}map[visit]bool
復制代碼
比較過程中,一旦發現比較的“對”,已經在 map 里出現過的話,直接判定“深度”比較結果的是 true
。
總結
Go 作為一門靜態語言,相比 Python 等動態語言,在編寫過程中靈活性會受到一定的限制。但是通過接口加反射實現了類似于動態語言的能力:可以在程序運行時動態地捕獲甚至改變類型的信息和值。
Go 語言的反射實現的基礎是類型,或者說是 interface,當我們使用反射特性時,實際上用到的就是存儲在 interface 變量中的和類型相關的信息,也就是常說的 <type, value>
對。
只有 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 還原成原先的接口變量;
- 反射可以用來修改一個變量的值,前提是這個值可以被修改。
參考資料
【維基百科中文】zh.wikipedia.org/wiki/反射_(計算…
【碼洞老錢 反射】juejin.im/post/5c2040…
【Go官方博客 reflection】blog.golang.org/laws-of-ref…
【GCTT譯文,不錯】mp.weixin.qq.com/s/dkgJ_fA0s…
【json庫 源碼分析】zhuanlan.zhihu.com/p/37165706
【reflect 代碼例子和圖比較好】blog.gopheracademy.com/advent-2018…
【反射使用講得不錯】juejin.im/post/5a75a4…
【接口和反射的關系 ,english】blog.gopheracademy.com/advent-2018…
【總結成知識點】www.cnblogs.com/susufufu/p/…
【Type Value】colobu.com/2016/07/09/…
【講得比較清晰簡單】www.lijiaocn.com/編程/2017/11/…
【DeepEqual】github.com/Chasiny/Blo…
【反射使用場景】yq.aliyun.com/articles/59…