Golang 反射
什麼是反射
反射?光的反射嗎?在物理學中有反射這個概念,表示光在分界面上改變傳播方向又返回原來物質中的現象。本文講述的是反射是在計算機領域的概念,在計算機科學中,反射編程(reflective programming)定義如下,這是來做維基百科的描述。
❝
In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
❞
翻譯過來是說,反射編程或反射是指程序在運行時,可以檢查、訪問和修改它結構(內存佈局)或行爲的一種能力。簡單來說就是在程序運行的過程中訪問和修改它自己。具體來說,可執行程序是由數據 + 指令構成的,修改它的結構即修改數據,對應到源代碼中就是修改數據結構和變量內容,修改它的行爲即修改指令,對應到源代碼中就是修改調用函數。
想一想,如果是讓你實現這個反射功能怎麼做呢?也許有讀者會說,那可以這樣做,要反射程序 A,我直接寫個程序在程序 A 運行的過程中直接修改他的內存數據和指令,確實可以,不過難度很高,這是黑客玩的。對於我們來說,希望有一種技術機制能夠通過編程來實現,在 Golang 中,標準庫提供了 reflect 包,該包提供了 API 讓我們可以很方便進行反射編程。
爲什麼需要反射
- 反射能夠讓程序動態的適應不同的情況,讓程序更具通用能力
也許有讀者說,用接口也可以呀,確實將參數定義成接口,給接口傳不同的實參,就能夠動態的調用對應的函數。面向接口編程確實可以以簡化編寫分別適用於多種不同情形的功能代碼,但是反射可以解決比面向接口編程更加普通的場景,下面舉一個例子。有時候我們需要編寫一個函數能夠處理一類並不滿足普通公共接口的類型的值,也可能是因爲它們並沒有確定的表示方式,或者是在我們設計該函數的時候這些類型可能還不存在。在 fmt 庫中有一個 Sprint 函數,fmt.Sprint 入參是一個 interface{} 可變參數,返回一個 string 對象,它可以用來對任意類型的值進字符串輸出,甚至是用戶自定義的類型。下面我們也來嘗試實現一個類似功能的函數,這裏簡化處理,只接受一個參數不支持可變參數,然後返回一個 string。
type stringer interface {
String() string
}
func MySprint(a interface{}) string {
switch a := a.(type) {
case stringer:
return a.String()
case string:
return a
case int:
return strconv.Itoa(a)
case int8:
return strconv.Itoa(int(a))
case bool:
if a {
return "true"
} else {
return "false"
}
default:
return "<unknown>"
}
}
如果傳入的類型是 int16, int32, int64, []int, map[string]string 等等類型呢?我們當然可以添加更多的測試分支,但是這些組合類型的數目基本是無窮的。還有如何 處理類似於 url.Values 這樣的具體的類型呢?url.Values 類型定義如下:
// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
type Values map[string][]string
Values 是基於 map[string][]string 創建的新類型,它和 map[string][]string 是不同的類型,底層的元數據信息是不同的。在 Golang interface 知多少有分析, 也就是說 url.Values 並不能匹配 map[string][]string 類型,而且 switch 類型分支也不可能包含每個類似 url.Values 的類型,這會導致對這些庫的依賴。沒有辦法來檢查未知類型了, 但是使用反射就能搞定這種情況,這是爲什麼需要反射的原因。
反射是怎麼實現的
根據反射編程的定義,是在運行時能夠檢查、訪問和修改對象的內存佈局、調用方法的行爲。這裏的對象是泛指的概念,可以是內置類型變量,可以是自定義結構體變量,也可以是函數變量。在計算機中, 對一個對象我們可以從類型 + 值兩個維度去描述它。對一個結構體對象來說,類型描述了結構體的名稱,它有哪些字段,有哪些方法,對齊方式等等,對一個函數對象來說,類型需要描述函數名稱,函數的入參信息, 函數的出參信息等。值讓我們可以有途徑修改它的內容,修改它的內存佈局,方法地址等等。Golang 的 reflect 包正是提供了一系列函數和方法 API,可以方便的獲取對象的類型和修改它的值。這裏可以讀者稍做停頓,想一想,如果是我自己來設計 reflect 的 API, 我該怎麼做?根據上面的介紹,對任意一個對象,可以獲取到它的類型和值描述性,所以要定義兩個函數,簽名如下
func typeOf(i interface{}) Type{
...
}
func valueOf(i interface{}) Value{
...
}
兩個函數的入參要定義成 interface{} 類型,因爲只有定義成 interface{} 類型,纔可以接受任意類型的參數。反射值 Type 和 Value 分別描述類型和值信息。
下面分析 reflect 包,從源碼的角度分析反射的實現。「說明,下面的分析基於的 go 版本是 1.14」
reflect 包兩個核心結構是 reflect.Type 和 reflect.Value,對應到上面就是從類型 + 值去描述一個對象,這兩種類型都是可以導出,在外面可以使用它們,構建反射編程。
reflect.Type
reflect.Type 是一個接口類型,定義了一系列函數從各個方面去描述對象的類型信息,例如結構體的名稱是什麼,佔用的大小是多少,有多個個字段。該接口一共定義了 31 個方法,其中 29 個可導出類型,另外 2 個是不可導出,是包內使用的。在 29 個可導出類型中,可以分爲兩部分,一部分是對所有的類型都能使用的方法(下表中標註的通用),另一部分的方法是有適用範圍的,只有某些甚至某個類型才能調用,不正確的類型調用將引發 panic。具體類型說明下表。
reflect.Type 接口定義的接口詳細的功能說明如下源碼註解,爲什麼將 Type 定義成接口而不是結構體,一個可解釋的說法是,標準庫的作者不想讓使用者關注它的方法外的其他內容,Type 是從各個方面描述對象的類型信息,是隻讀的,使用只管調用提供的方法就可以了,這體現了 go 語言的哲學思想,”less is more“的原則。
// Type是一個接口類型,描述了Go對象的類型信息。所有信息的獲取通過Type提供的方法,
// Type涵蓋了所有類型對象的信息,包括已知的類型int,map還是未知的自定義類型Mystruct
// 除了一些通用的信息,描述了類型都具有的信息,對特定的類型,例如channel, 它具有
// 方向屬性,所以Type中定義的函數,有些是通用的有些是對特定類型才能使用的,
// 建議我們在調用方函數之前,可以調用Kind方法判斷一下類型,以防止不合時宜的調用會
// 引發panic
// Type類型值是可以進行比較的,即可以進行==操作,所以它可以作爲map的key
// Type比較基於他們代表的類型,如果代表的類型相同,兩個Type的值是相等的
type Type interface {
// 返回該類型在內存中分配時佔用大小,單位爲字節
Align() int
// 返回該類型作爲結構中的一個字段時佔用的內存大小,單位爲字節
FieldAlign() int
// 根據輸入的下標索引返回類型定義中的第i個方法,輸入下標的範圍爲[0,NumMethod())
// 傳入的參數超過這個範圍將引發panic
// 對於一個非接口類型的T或*T,返回Method的Type和Func,fields字段描述的是一個函數,這個
// 函數的第一個參數是接收者。
// 對於一個接口類型,返回的Method的Type字段表示方法的簽名,沒有接收者信息,Func字段爲nil。
// Method返回的是可導出的方法,這些方法是按字典序排過順序的,也就該方法是冪等的。
Method(int) Method
// MethodByName也是返回一個方法,功能與上面的Method(int) Method類型,不同的是
// 上面是根據傳入的下標返回,這裏是根據傳入的函數名稱返回與名稱一致的方法,如果不存在
// 傳入名稱的方法,第二個bool參數返回false表示未找到。
MethodByName(string) (Method, bool)
// 返回可導出的方法數量
NumMethod() int
// 對於定義的類型,Name返回包內部類型的名稱,對於非定義的類型,Name返回""
Name() string
// PkgPath返回定義類型的包的路徑,也就是import導入時路徑,它唯一標識了包的路徑類型
// 例如"encoding/base6e", 如果是預先聲明的string/error或者沒有定義的 *T/struct{}/[]int,
// 或A(A是一個非定義類型的別名),PkgPath將返回""
PkgPath() string
// Size等同於unsafe.Sizeof(xx), 它返回了該類型的值佔用的內存大小,單位爲字節
Size() uintptr
// String返回類型的字符串表示,這個字符串可能是一個簡寫的形式,例如對於encoding/base64返回的是base64
// 並不能保證唯一性,也是說兩個不同的類型,他們String返回的內容是一樣的
// 如果是測試比較類型,可以直接比較類型Type
String() string
// Kind返回該類型具體類型,每種基礎類型都有唯一的Kind值
Kind() Kind
// Implements表示該類型是否實現了接口類型u
Implements(u Type) bool
// AssignableTo表示該類型的值能否賦值給類型u
AssignableTo(u Type) bool
// ConvertibleTo表示該類型的值能否轉換成u類型
ConvertibleTo(u Type) bool
// Comparable表示該類型的值是否是可以比較的
Comparable() bool
// 下面的方法是不是通用的,依賴於Kind,也就是某個方法對於特定的類型才能使用
// Bits對Int*,Uint*,Float*,Complex*才能使用
// Bits以bits單位返回類型的大小,只有對sized或unsized的Int,Uint,Float和Complex
// 調用纔是正確的,否則將引發panic
Bits() int
// ChanDir返回channel類型的方向信息,是否雙向的、只可發送、只可接受
// 對不是channel類型的調用該方法將panic
ChanDir() ChanDir
// IsVariadic表示函數的最後一個入參是否是變參類型,如果該類型不是一個Func類型
// 調用該方法將會panic。
// IsVariadic使用場景,對於 func(x int, y ...float64)函數 t, 調用IsVariadic會
// 返回true, t.NumIn()=2是說它有2個入參,t.In(0)表示第一個參數,它的Type爲"int",
// t.In(1)表示第二個參數,他的類型爲[]float64
IsVariadic() bool
// 返回一個該類型的元素的Type
// 如果該類型不是Array,Chan,Map,Ptr,Slice, 調用Elem會引發panic
Elem() Type
// Field返回結構體類型的第i個字段,如果該類型不是struct,調用會
// 引發panic, 下標i的範圍爲[0,NumField())
Field(i int) StructField
// FieldByIndex根據輸入的下標序列返回對應的嵌套字段,相當於對每個index調用Field
// 該函數只適用用struct類型,對非struct調用會panic
FieldByIndex(index []int) StructField
// FieldByName根據輸入名稱返回對應的字段,如果沒有找到,第二個返回參數返回false
FieldByName(name string) (StructField, bool)
// FieldByNameFunc根據輸入的函數匹配結構體中的字段,match函數接收一個string入參,
// 看結構體的字段是否滿足match函數,匹配順序是先在當前層匹配字段,然後在字段是內嵌的字段中
// 查找,就是先按光度查找,然後在按深度查找,最終停止在含有一個或多個滿足match函數的結構體中
// 如果在該深度上滿足match函數的字段有多個,這些字段會互相取消,並且返回沒有匹配,這種行爲
// 反映了go在有內嵌字段的結構的情況下對名稱查找的處理方式。
FieldByNameFunc(match func(string) bool) (StructField, bool)
// 返回函數第i個入參的類型,如果調用者的類型不是函數,將引發panic.
// 如果傳入的值不在[0,NumIn())範圍內,也會發生panic。
In(i int) Type
// 返回map類型的key的類型,如果調用者類型不是map類型,將引發panic.
Key() Type
// 返回數組類型的長度,如果調用者的類型不是數組,會引發panic.
Len() int
// 返回結構體中字段的數量,如果調用者的類型不是結構體,會發生panic.
NumField() int
// 返回函數的入參數量,如果調用者的類型(kind)不是函數,會發生panic.
NumIn() int
// 返回函數的出參數量,如果調用者的類型(kind)不是函數,會發生panic.
NumOut() int
// Out返回一個函數類型的第i個出參類型,如果類型的Kind不是函數,會引發panic,
// 如果i不在[0,NumOut())範圍內,也會發生panic.
Out(i int) Type
// 下面兩個函數是不可導出類型,給包內部使用的
common() *rtype
uncommon() *uncommonType
}
reflect.Value
reflect.Value 是一個結構體,並且是可以導出的,它表示的是將一個接口反射成一個 go 類型值,Value 結構體有很多個方法,下面會挑幾個作爲實例說明,需要說明的是並不是任何類型都可以 調用 Value 定義的所有方法,在調用方法前看下前面的說明,是哪些類型支持該方法的調用,先用 Kind 方法判斷 Value 的類型,和 reflect.Type 一樣,調用類型不匹配的方法會引發 panic。Value 結構體非常簡單,只有 4 個字段,詳細說明見下面的代碼註釋分析。
// Value是一個結構體,表示的是將一個接口反射成一個go 類型的值
// Value結構體有很多個方法,並不是任何類型都可以調用所有的方法,每個方法可以使用的
// 類型在方法的前面都有說明,在調用特定種類的方法前,最好先用Kind方法判斷Value的
// 類型,和reflect.Type一樣,調用類型不匹配的方法會引發panic.
// zero Value表示沒有值,它的IsValid方法會返回false,Kind方法返回Invalid,
// String方法返回"",剩下的其他方法都會產生panic.大部分函數和方法都不會返回
// invalid Value.如果確實返回了invalid value, 在文檔中會明確說明特殊條件。
// Value類型的比較,比較的是他們的Interface方法,使用==比較Value並不會比較
// 他們底層表示的值。
type Value struct {
// typ holds the type of the value represented by a Value.
// typ指向Value表示的值的類型
typ *rtype
// 指向值的指針,也就是間接指向數據,如果設置了flagIndir,ptr則是指向數據的指針。
// 只有當設置了flagIndir或typ.pointers()爲true時有效。
ptr unsafe.Pointer
// flag保存了value的元數據信息,最低位是標誌位:
// -flagStickyRO 通過未導出的未嵌入字段獲取,因此是隻讀的
// -flagEmbedRO 通過未導出的嵌入字段獲取,因此爲只讀的
// -flagIndir val保存指向數據的指針
// -flagAddr v.CanAddr爲true表示flagIndir
// -flagMethod v是值方法
// 接下來的5個bit表示value的Kind類型,除了方法的values外,它會重複typ.Kind
// 其餘23位以上給出了方法values的方法編號,如果flag.kind()!=Func, 可以假定
// flagMethod沒有設置,如果ifaceIndir(typ),可以假定設置了flagIndir.
flag
// 一個方法的value表示一個相關方法的調用,像一些方法接收者r調用r.Read
// typ+val+flag比特位描述了接收者r,但是Kind標記位表示Func(表示是一個函數),
// 並且該標誌的高位給出r的類型的方法集中的方法編號。
}
Bool 方法返回它的底層值,只能對 v 的 kind 爲 Bool 的 value 調用,否則將引發 panic.
func (v Value) Bool() bool {
v.mustBe(Bool)
return *(*bool)(v.ptr)
}
Bytes 方法返回一個字節數組,這個字節數組的值是 v 底層的值,如果 v 的底層值不是一個字節切片會產生 panic.
func (v Value) Bytes() []byte {
v.mustBe(Slice)
if v.typ.Elem().Kind() != Uint8 {
panic("reflect.Value.Bytes of non-byte slice")
}
// Slice is always bigger than a word; assume flagIndir.
return *(*[]byte)(v.ptr)
}
CanAddr 表示 v 的地址是否可以通過 Addr 方法獲取,如果 v 的元素是切片,可以尋址的數組,可以尋址的結構體字段,則 v 是可以尋址的,CanAddr 會返回 true. 當 v 是不可以尋址的,即調用 CanAddr 會返回 false, 這時調用 Addr 將會產生 panic.
func (v Value) CanAddr() bool {
return v.flag&flagAddr != 0
}
上面從源碼的角度介紹了 reflect 包兩個重要類型, reflect.Type 接口和 reflect.Value 方法。下面分析這兩個類型產生的方法,reflect.TypeOf() 和 reflect.ValueOf()
reflect.TypeOf 函數
先看 TypeOf 的簽名,它的入參是一個空接口類型,也就說任何類型都可以傳給 TypeOf 函數,它的返回值是 Type 接口,Type 接口在前面已詳細分析過了。下面結合源碼看的內部是怎麼實現的。非常簡單隻有 2 行邏輯,先將空接口 i 轉成類型 emptyInterface, 然後調用 toType 方法。emptyInterface 是一個結構體,它的定義也在下面給出了,可以看到包含一個 rtype 字段和一個 unsafe.Pointer 字段,繼續看 rtype 的定義,咦,有沒有很熟悉?這裏的 rtype 定義與 eface 裏面的_type 是一模一樣的,確實他們就是一樣的,// rtype must be kept in sync with ../runtime/type.go:/^type._type. 註釋也說了 rtype 要與 runtime/type.go 中的_type 保持一致。所以將空接口 i 轉成 emptyInterface 完全沒有問題,它們的定義都是一樣的。這裏可能會有讀者問,既然 rtype 與 runtime/type.go 中的_type 一模一樣,那爲啥在 reflect 包中還要重新定義一遍呢?對就是不想引用 runtime 包,造成對 runtime 包的依賴,形成 reflect 包和 runtime 包的耦合,所以這裏直接重新定義一個結構一樣的類型。toType 只是對 eface.typ 做了判空操作,eface.typ 即 rtype 類型,*rtype 類型已實現了 Type 的所有方法,所以 eface.typ 已經是一個 Type 了。有一點需要注意的是,如果傳遞給 i 的是一個空接口,TypeOf 將返回 nil, 這時對返回值直接調用方法會引發 panic。
func TypeOf(i interface{}) Type {
// 強行將interface{}類型的i轉成emptyInterface
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
type rtype struct {
size uintptr
ptrdata uintptr // number of bytes in the type that can contain pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldAlign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
// 將*rtype類型的t轉成Type接口類型,如果t是爲空指針,返回一個Type類型的nil.
// 在gc中,唯一關心的是nil的*rtype必須轉成nil Type, 在gccgo中,需要確保相同
// 類型的多個*rtype合併成一個Type
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}
對空接口 i 來說,那任何類型都可以傳給他。具體點就是有兩種類型,一種是 interface 變量,另一種是具體的類型變量。interface 變量包含空接口 interface{} 變量和自定義接口變量,具體類型變量包括內置類型 int8,int16, 自定義結構體類型變量,還有函數類型變量。如果 i 是 interface 變量,並且被賦值了具體類型的變量,TypeOf 返回的是 i 被賦值變量類型(具體類型)的動態類型信息,如果 i 沒有被賦值任何具體的類型變量,返回的是接口自身的靜態類型信息。如果 i 是具體的類型信息,返回的是具體類型信息。下面通過一個例子加深理解。
import (
"fmt"
"reflect"
)
type Animal interface {
Say() string
Walk()
}
type cat struct{}
func (c cat) Say() string {
return "喵喵喵..."
}
func (c cat) Walk() {
fmt.Println("我走起來靜悄悄")
}
func main() {
var animal Animal
animal = cat{}
fmt.Println(reflect.TypeOf(animal).Name())
fmt.Println(reflect.TypeOf(animal).Kind().String())
animal2 := new(Animal)
fmt.Println(reflect.TypeOf(animal2).Elem().Name())
fmt.Println(reflect.TypeOf(animal2).Elem().Kind().String())
cat := cat{}
fmt.Println(reflect.TypeOf(cat).Name())
fmt.Println(reflect.TypeOf(cat).Kind().String())
}
運行結果如下
第一組 animal 綁定了 cat 對象,所以它輸出的類型是被綁定對象類型 cat,cat 是一個結構體,所以它的 kind 輸出爲 struct, 第二組 animal2 是一個 Animal 接口,反射後元素的類型是 Animal,它的類型是 interface, 第三組,傳遞的是一個具體類型,反射返回的是具體類型的 cat,kind 爲 struct.
reflect.ValueOf 函數
reflect.ValueOf 返回的是一個結構體對象,傳入參數類型是 interface{}, 也就是可以傳入任何類型的變量,如果 i 是一個 nil 值 (接口爲 nil 當且僅當它的類型和值都爲 nil),ValueOf 返回 Value{}, 對於非 nil 值,它會根據 i 的具體值進行初始化。它的內部實現邏輯是先調用 escapes 確保 i 逃逸到堆上,然後調用 unpackEface 將 i 轉成 Value 對象。unpackEface 將 emptyInterface 轉換成 Value。實現分爲 3 步,先將入參 interface 強轉成 emptyInterface,然後判斷 emptyInterface.typ 是否爲空,如果不爲空才能讀取 emptyInterface.word。最後拼裝 Value 數據結構中的三個字段,*rtype,unsafe.Pointer,flag。
// ValueOf返回一個新的Value, 新Value根據入參i的具體值進行初始化
// ValueOf(nil)放回Value{}
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
// 保證i逃逸到堆上
escapes(i)
// 將空接口i轉成一個Value對象
return unpackEface(i)
}
func unpackEface(i interface{}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
// NOTE: don't read e.word until we know whether it is really a pointer or not.
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}
反射著名三定律
-
「reflection goes from interface value to reflection object」
定律 1:反射可以從接口值中得到反射對象
這裏要從 reflect 包的角度去看,反射對象指 reflect.Type 和 reflect.Value。對應到的函數就是就是上面介紹的 reflect.ValueOf 函數和 reflect.TypeOf 函數,它們 分別將接口 interface(入參) 轉換成 reflect.Type 和 reflect.Value。
-
「reflection goes from reflection object to interface value」
定律 2:反射可以從反射對象中獲得接口的值
定律 2 是定律 1 的逆過程,從前面 reflect.Value 結構體定義中國可以知道它包含了類型和值的信息,所以是可以將 Value 轉成接口值的,通過 reflect.Value 提供了 Interface 方法。
下面結合源碼進行簡要分析,Interface 是 Value 的一個方法,它將 Value v 轉成 interface{}, 內部調用的是 packEface 函數,該函數將根據 v.typ 和 v.ptr 來填充 interfade 的_type 和 data 字段。
func (v Value) Interface() (i interface{}) {
return valueInterface(v, true)
}
func valueInterface(v Value, safe bool) interface{} {
if v.flag == 0 {
panic(&ValueError{"reflect.Value.Interface", Invalid})
}
if safe && v.flag&flagRO != 0 {
// 不允許通過接口訪問不可導出的value,因爲它指向的地址可能是不可修改的或者方法或是函數是不被調用的
panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
}
if v.flag&flagMethod != 0 {
v = makeMethodValue("Interface", v)
}
if v.kind() == Interface {
if v.NumMethod() == 0 {
return *(*interface{})(v.ptr)
}
return *(*interface {
M()
})(v.ptr)
}
// 將Value v轉換成interface{}
return packEface(v)
}
func packEface(v Value) interface{} {
t := v.typ
var i interface{}
e := (*emptyInterface)(unsafe.Pointer(&i))
// 填充e的各個字段,也就是i的各個字段,因爲他們指向同一個地方
switch {
case ifaceIndir(t):
if v.flag&flagIndir == 0 {
panic("bad indir")
}
ptr := v.ptr
if v.flag&flagAddr != 0 {
c := unsafe_New(t)
typedmemmove(t, c, ptr)
ptr = c
}
e.word = ptr
case v.flag&flagIndir != 0:
e.word = *(*unsafe.Pointer)(v.ptr)
default:
// Value is direct, and so is the interface.
e.word = v.ptr
}
e.typ = t
return i
}
-
「to modify a reflection object, the value must be settable」
定律 3:想要修改反射對象,它的值必須是可修改的。
首先理解一點反射對象代表的是反射前的變量,對反射對象的修改要能夠影響到原變量,如果不能修改原變量,這個是不被允許的。Value 對象提供了一系列 SetXX 方法來修改反射對象。
下面的代碼會在運行時發生 panic, 會提示 panic: reflect: reflect.flag.mustBeAssignable using unaddressable value 這裏給的提示信息是 value 是不可尋址的。在下面代碼中,調用 reflect.ValueOf 傳進去的是一個值類型的變量,獲得的 Value 其實是完全的值拷貝,這個 Value 是不能被修改的。如果傳進去是一個指針,獲得的 Value 是一個指針副本,但是這個指針指向的地址的對象是可以改變的。
func main() {
var i int = 10
v := reflect.ValueOf(i)
v.SetInt(11) //引發panic
}
// 下面這裏傳入的是i地址,是可以尋址的,正常運行,i的值會被修改爲11
func main() {
var i int = 10
v := reflect.ValueOf(&i)
fmt.Println("type of v ", v.Type())
fmt.Println("can set of v ", v.CanSet())
e := v.Elem()
e.SetInt(11)
fmt.Println(i)
}
其他轉換
-
Type 和 Value 相互轉換
Type 類型描述的是對象的類型,不含有值信息,所以 Type 是不能直接轉換 Value 對象的,可以通過 reflect.New(typ Type) Value 函數創建一個指向 Type 類型的對象,不過這個產生的對象是默認值,即該類型的零值。反射對象 Value 中已含有 Type 的信息,Value 直接提供了轉換函數。func(v Value) Type() Type{ ... }
-
指針型 Value 與值 Value 相互轉換
指針的 Value 轉換成值 Value 可以使用 Indirect() 和 Elem() 方法,值 Value 轉換成指針的 Value 採用 Addr() 方法,內部實現這裏不在展開分析,感興趣的讀者可以看源碼。
上面所有的轉換關係都包含在了下面這張圖中,看這個圖可以串聯起所有的反射知識點。
反射使用場景
反射在框架和庫中應用的比較多,例如標準庫中經典的 json 序列化函數 Marshal 和 Unmarshal 函數,對序列化和反序列化函數,需要知道參數的全名字段,參數的類型和值,在調用它的 get 和 set 函數進行實際的操作。
在結構體的深拷貝可以採用反射實現。這裏引申一個問題,深拷貝一個結構體可以有哪些實現方法?
方法一可以採用序列化,將結構體 (或者說對象) 進行序列化是很多語言深拷貝的常用方式,採用 json 序列化或 gob 序列化等.
方法二採用反射,c++ 和 java 語言提供了深拷貝的方法,在 c++ 中,可以使用 memcpy 函數進行深拷貝,在 java 中實現 clone 方法進行。go 標準庫中沒有提供類似的函數,可以使用 reflect 包實現深拷貝。
還有在一些硬編碼的地方可以嘗試使用 reflect 減少編碼量,像在實現打印輸出的地方,要處理各種不同類型的輸入參數,可以採用 reflect 大大提高生產力。
總結
-
反射提供了在運行時可以訪問查看、檢查和修改對象內存佈局以及函數調用的能力。
-
使用反射可以在一定程度上避免硬編碼,提供了程序的可擴展性。
但不建議大量在生產代碼中大量使用反射,一個首要原因是可讀性很差,還有就是學習成本比較高,面向反射的編程需要較多的高級知識,包括框架、關係映射和對象交互,以實現更通用的代碼執行。還有就是將部分信息檢查工作從編譯期推遲到了運行期,調用方法和引用對象並非直接的地址引用,而是通過 reflect 包提供的一個抽象層間接訪問。此舉在提高了代碼靈活性的同時,犧牲了一點點運行效率。在項目性能要求較高的地方,一定要慎重考慮使用反射。由於逃避了編譯器的嚴格檢查,所以一些不正確的修改會導致程序 panic。
Golang 提供的 reflect 包可以看做是對 runtime 中 interface 的應用包裝,想要靈活運用反射技術,先要理解 interface 知識,筆者也分享了對 interface 的知識講解文章 <Golang interface 知多少 >,這兩篇可以聯合起來看,可以更好理解。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Jq4JFARDQnKxoxJELl52nw