出品 | 滴滴技術html
做者 | 饒成全程序員
前言:Go 做爲一門靜態語言,相比 Python 等動態語言,在編寫過程當中靈活性會受到必定的限制。可是經過接口加反射實現了相似於動態語言的能力:能夠在程序運行時動態地捕獲甚至改變類型的信息和值。web
▍閱讀索引編程
什麼是反射json
爲何要用反射數組
反射是如何實現的瀏覽器
types 和 interfacebash
反射的基本函數服務器
反射的三大定律網絡
反射相關函數的使用
代碼樣例
未導出成員
反射的實際應用
json 序列化
DeepEqual 的做用及原理
總結
參考資料
直接看維基百科上的定義:
在計算機科學中,反射是指計算機程序在運行時(Run time)能夠訪問、檢測和修改它自己狀態或行爲的一種能力。用比喻來講,反射就是程序在運行的時候可以「觀察」而且修改本身的行爲。
那我就要問個問題了:不用反射就不能在運行時訪問、檢測和修改它自己的狀態和行爲嗎?
問題的回答,其實要首先理解什麼叫訪問、檢測和修改它自己狀態或行爲,它的本質是什麼?
實際上,它的本質是程序在運行期探知對象的類型信息和內存結構,不用反射能行嗎?能夠的!使用匯編語言,直接和內層打交道,什麼信息不能獲取?可是,當編程遷移到高級語言上來以後,就不行了!就只能經過 反射來達到此項技能。
不一樣語言的反射模型不盡相同,有些語言還不支持反射。《Go 語言聖經》中是這樣定義反射的:
Go 語言提供了一種機制在運行時更新變量和檢查它們的值、調用它們的方法,可是在編譯時並不知道這些變量的具體類型,這稱爲反射機制。
須要反射的 2 個常見場景:
一、有時你須要編寫一個函數,可是並不知道傳給你的參數類型是什麼,多是沒約定好;也多是傳入的類型不少,這些類型並不能統一表示。這時反射就會用的上了。
二、有時候須要根據某些條件決定調用哪一個函數,好比根據用戶的輸入來決定。這時就須要對函數和函數的參數進行反射,在運行期間動態地執行函數。
在講反射的原理以及如何用以前,仍是說幾點不使用反射的理由:
一、與反射相關的代碼,常常是難以閱讀的。在軟件工程中,代碼可讀性也是一個很是重要的指標。
二、Go 語言做爲一門靜態語言,編碼過程當中,編譯器能提早發現一些類型錯誤,可是對於反射代碼是無能爲力的。因此包含反射相關的代碼,極可能會運行好久,纔會出錯,這時候常常是直接 panic,可能會形成嚴重的後果。
三、反射對性能影響仍是比較大的,比正常代碼運行速度慢一到兩個數量級。因此,對於一個項目中處於運行效率關鍵位置的代碼,儘可能避免使用反射特性。
interface是 Go 語言實現抽象的一個很是強大的工具。當向接口變量賦予一個實體類型的時候,接口會存儲實體的類型信息,反射就是經過接口的類型信息實現的,反射創建在類型的基礎上。
Go 語言在 reflect 包裏定義了各類類型,實現了反射的各類函數,經過它們能夠在運行時檢測類型的信息、改變類型的值。
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 Readerinterface{
Read(p []byte) (n int, err error)
}
type Writerinterface{
Write(p []byte) (n int, err error)
}
複製代碼
接下來,就是接口之間的各類轉換和賦值了:
varr io.Reader
tty, err := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)
iferr != nil{
returnnil, 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 接口。
所以下面的斷言語句能夠執行:
varw io.Writer
w = r.(io.Writer)
複製代碼
之因此用斷言,而不能直接賦值,是由於 r 的靜態類型是 io.Reader,並無實現 io.Writer 接口。斷言可否成功,看 r 的動態類型是否符合要求。
這樣,w 也能夠表示成 <tty, *os.File>,僅管它和 r 同樣,可是 w 可調用的函數取決於它的靜態類型 io.Writer,也就是說它只能有這樣的調用形式: w.Write() 。
w 的內存形式以下圖:
和 r 相比,僅僅是 fun 對應的函數變了: Read->Write。
最後,再來一個賦值:
varempty 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() {
varr 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 賦值
varw 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 賦值
varempty interface{}
empty = w
fmt.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))
returntoType(eface.typ)
}
複製代碼
這裏的 emptyInterface 和上面提到的 eface 是一回事(字段名略有差別,字段是相同的),且在不一樣的源碼包:前者在 reflect 包,後者在 runtime 包。 eface.typ 就是動態類型。
type emptyInterface struct{
typ *rtype
word unsafe.Pointer
}
複製代碼
至於 toType 函數,只是作了一個類型轉換:
func toType(t *rtype) Type{
ift == nil{
return nil
}
returnt
}
複製代碼
注意,返回值 Type 其實是一個接口,定義了不少方法,用來獲取類型相關的各類信息,而 *rtype 實現了 Type 接口。
type Typeinterface{
// 全部的類型均可以調用下面這些函數
// 此類型的變量對齊後所佔用的字節數
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{
ifi == nil{
return Value{}
}
// ……
return unpackEface(i)
}
// 分解 eface
func unpackEface(i interface{}) Value{
e := (*emptyInterface)(unsafe.Pointer(&i))
t := e.typ
ift == 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。
第三條不太好懂:若是須要操做一個反射變量,那麼它必須是可設置的。反射變量可設置的本質是它存儲了原變量自己,這樣對反射變量的操做,就會反映到原變量自己;反之,若是反射變量不能表明原變量,那麼操做了反射變量,不會對原變量產生任何影響,這會給使用者帶來疑惑。因此第二種狀況在語言層面是不被容許的。
舉一個經典例子:
varx 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 都是可被設置的。
就像在通常的函數裏那樣,當咱們想改變傳入的變量時,使用指針就能夠解決了。
varx 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 Childstruct{
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
ifv.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
fori := 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
fori := 0; i < v.Len(); i++ {
e := v.Index(i)
handsome := e.FieldByName("Handsome")
// 尋找字段名爲 Name 或者 tag 的值爲 Name 的字段
varname reflect.Value
forj := 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: [{SteveClerktrue} {qcrao GoProgrammerfalse}]
adults after handsome: [{SteveClerktrue} {qcrao GoProgrammertrue}]
-------------
children before handsome: [{Ava3true} {qcrao 6false}]
children after handsome: [{Ava3true} {qcrao 6true}]
複製代碼
代碼主要作的事情是:找出傳入的參數爲 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 函數。
開發過 web 服務的同窗,必定用過 json 數據格式。 json 是一種獨立於語言的數據格式。最先用於瀏覽器和服務器之間的實時無狀態的數據交換,並由此發展起來。
Go 語言中,主要提供 2 個函數用於序列化和反序列化:
func Marshal(v interface{}) ([]byte, error)
func Unmarshal(data []byte, v interface{}) error
複製代碼
兩個函數的參數都包含 interface,具體實現的時候,都會用到反射相關的特性。
對於序列化和反序列化函數,均須要知道參數的全部字段,包括字段類型和值,再調用相關的 get 函數或者 set 函數進行實際的操做。
在測試函數中,常常會須要這樣的函數:判斷兩個變量的實際內容徹底一致。
例如:如何判斷兩個 slice 全部的元素徹底相同;如何判斷兩個 map 的 key 和 value 徹底相同等等。
上述問題,能夠經過 DeepEqual 函數實現。
func DeepEqual(x, y interface{}) bool
複製代碼
DeepEqual 函數的參數是兩個 interface,實際上也就是能夠輸入任意類型,輸出 true 或者 flase 表示輸入的兩個變量是不是「深度」相等。
先明白一點,若是是不一樣的類型,即便是底層類型相同,相應的值也相同,那麼二者也不是「深度」相等。
type MyIntint
type YourIntint
func main() {
m := MyInt(1)
y := YourInt(1)
fmt.Println(reflect.DeepEqual(m, y)) // false
}
複製代碼
上面的代碼中,m, y 底層都是 int,並且值都是 1,可是二者靜態類型不一樣,前者是 MyInt,後者是 YourInt,所以二者不是「深度」相等。
在源碼裏,有對 DeepEqual 函數的很是清楚地註釋,列舉了不一樣類型,DeepEqual 的比較情形,這裏作一個總結:
類型
深度相等情形
通常狀況下,DeepEqual 的實現只須要遞歸地調用 == 就能夠比較兩個變量是不是真的「深度」相等。
可是,有一些異常狀況:好比 func 類型是不可比較的類型,只有在兩個 func 類型都是 nil 的狀況下,纔是「深度」相等;float 類型,因爲精度的緣由,也是不能使用 == 比較的;包含 func 類型或者 float 類型的 struct, interface, array 等。
對於指針而言,當兩個值相等的指針就是「深度」相等,由於二者指向的內容是相等的,即便二者指向的是 func 類型或者 float 類型,這種狀況下不關心指針所指向的內容。
一樣,對於指向相同 slice, map 的兩個變量也是「深度」相等的,不關心 slice, map 具體的內容。
對於「有環」的類型,好比循環鏈表,比較二者是否「深度」相等的過程當中,須要對已比較的內容做一個標記,一旦發現兩個指針以前比較過,當即中止比較,並斷定兩者是深度相等的。這樣作的緣由是,及時中止比較,避免陷入無限循環。
來看源碼:
func DeepEqual(x, y interface{}) bool{
ifx == nil|| y == nil{
returnx == 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 變量中的和類型相關的信息,也就是常說的 <type,value> 對。
只有 interface 纔有反射的說法。
反射在 reflect 包中實現,涉及到兩個相關函數:
func TypeOf( i interface{} ) Type
func ValueOf( i interface{} ) Value
複製代碼
Type 是一個接口,定義了不少相關方法,用於獲取類型信息。Value 則持有類型的具體值。Type、Value、Interface 三者間經過函數 TypeOf,ValueOf,Interface 進行相互轉換。
最後溫習一下反射三大定律:
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.
翻譯一下:
1. 反射將接口變量轉換成反射對象 Type 和 Value;
2. 反射能夠經過反射對象 Value 還原成原先的接口變量;
3. 反射能夠用來修改一個變量的值,前提是這個值能夠被修改。