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 可以用<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>
,僅管它和 r
一樣,但是 w 可調用的函數取決於它的靜態類型 io.Writer
,也就是說它只能有這樣的調用形式: w.Write()
。w
的內存形式如下圖:
和 r
相比,僅僅是 fun
對應的函數變了:Read -> Write
。
最後,再來一個賦值:
var empty interface{}
empty = w
由於 empty
是一個空接口,因此所有的類型都實現了它,w 可以直接賦給它,不需要執行斷言操作。
從上面的三張圖可以看到,interface 包含三部分信息:_type
是類型信息,*data
指向實際類型的實際值,itab
包含實際類型的信息,包括大小、包路徑,還包含綁定在類型上的各種方法(圖上沒有畫出方法),補充一下關於 os.File 結構體的圖:
這一節的最後,展示一個技巧:
先參考源碼,分別定義一個“僞裝”
的 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(&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 *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 (v Value)
就能看到。
另外,通過 Type()
方法和 Interface()
方法可以打通 interface
、Type
、Value
三者。Type() 方法也可以返回變量的類型信息,與 reflect.TypeOf() 函數等價。Interface() 方法可以將 Value 還原成原來的 interface。
總結一下: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 住原變量的地址才行。