手摸手Go 接口與反射

Go是強類型/靜態(tài)類型語言筹误,每個變量在編譯時就已經(jīng)確定是哪種靜態(tài)類型蟹但。反射(reflection)是程序在運行時可以訪問、檢測拂苹、修改自身狀態(tài)或行為的一種能力安聘。在Java出現(xiàn)后迅速流行起來的概念,Go也提供了這種在運行時更新、檢查變量值浴韭、調(diào)用變量的方法和變量支持的內(nèi)在操作的機制,一定程度上彌補了靜態(tài)語言在動態(tài)行為上的不足丘喻。

正常來講,程序在編譯時會將變量轉(zhuǎn)換為內(nèi)存地址念颈,變量名不會被編譯器寫入可執(zhí)行部分泉粉,那么運行時程序就無法獲取自身的信息。支持反射的語言則需要在程序編譯期將變量的反射信息榴芳,如字段名嗡靡、類型信息、結(jié)構(gòu)體信息等整合到可執(zhí)行文件中窟感,并給程序提供接口訪問反射信息讨彼。這樣程序運行時即可獲取類型的反射信息,并有能力操作修改它柿祈。

反射是把雙刃劍哈误,雖然代碼更加靈活了但是

  • 代碼閱讀起來也困難了
  • 一定程度上破壞了靜態(tài)類型語言的編譯期檢查 運行時會有panic風險
  • 降低了系統(tǒng)性能

我們?yōu)槭裁葱枰瓷洌?/h2>
  1. 無法預定義參數(shù)類型
  2. 函數(shù)需要根據(jù)入?yún)韯討B(tài)執(zhí)行

需要注意的是:Go中只有接口類型才可以反射,而反射又是建立在類型系統(tǒng)之上谍夭,so我們先來復習下類型與接口的知識

類型

Go是靜態(tài)類型語言黑滴。每個變量都有一個靜態(tài)類型,編譯時就已經(jīng)確定的類型:int紧索、float32袁辈、*MyType、[]byte等等

type MyInt int

var i int
var j MyInt

上面的栗子中珠漂,i與j具有不同的靜態(tài)類型(i是int類型晚缩,j為MyInt類型),盡管他們的基礎類型都是int媳危,但是他們之間不經(jīng)過轉(zhuǎn)換無法相互賦值荞彼。

類型的一個重要類別是接口類型,接口可以存儲任何非接口的具體值待笑,只要該值實現(xiàn)了接口方法即可鸣皂。

接口

接口是多個方法聲明的集合,側(cè)重于做什么暮蹂,不關(guān)系怎么做 誰來做寞缝。它更像是一種調(diào)用契約或協(xié)議(protocol)。接口解除了類型依賴仰泻,屏蔽了方法實現(xiàn)細節(jié)荆陆,但接口的實現(xiàn)機制存在運行時開銷。

Go的接口機制比較簡潔集侯,不像Java需要顯示聲明實現(xiàn)的接口被啼,Go只要目標類型方法集中包含了接口聲明的全部方法帜消,就被稱為實現(xiàn)了該接口,無須顯示聲明浓体。

如果一個接口沒有聲明任何方法泡挺,那么就是一個空接口interface{},類似JavaObject對象可以被賦值為任意類型的對象命浴。但

Go語言的接口類型不是任意類型 只是任意類型可以通過類型轉(zhuǎn)換成接口變量

接下來我們來看看接口的數(shù)據(jù)結(jié)構(gòu)粘衬,總結(jié)起來接口結(jié)構(gòu)如下:

interface structure

具體可以細分為

  • 不包含任何方法的接口interface{}
  • 包含一組方法的接口

Go語言使用runtime.eface表示不包含任何方法的接口,runtime.iface表示包含一組方法的接口咳促。

  1. 不包含任何方法的接口
type eface struct {
    _type *_type
    data  unsafe.Pointer
}
eface
  1. 包含一組方法的接口
type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
iface

可以看到不論空eface還是非空iface都包含了_type數(shù)據(jù)類型

type _type struct {
   size       uintptr //類型大小
   ptrdata    uintptr // 含有所有指針類型前綴大小
   hash       uint32 //類型hash值 避免在哈希表中計算
   tflag      tflag //額外類型信息標志
   align      uint8 // 類型變量對齊方式
   fieldalign uint8 // 類型結(jié)構(gòu)字段對齊方式
   kind       uint8 // 類型種類
  alg        *typeAlg //存儲hash和equal兩個操作 map的key就是適用key的_type.alg.hash(k)獲取的hash值
   // gcdata stores the GC type data for the garbage collector.
   // If the KindGCProg bit is set in kind, gcdata is a GC program.
   // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
   gcdata    *byte
   str       nameOff //類型名字的偏移
   ptrToThis typeOff
}

當然不同類型需要的描述是不一樣的,大多是利用_type組合其他基礎類型而成

接下來我們通過一個栗子拆解下接口內(nèi)存中的結(jié)構(gòu)究竟如何

type Animal interface {
    Say()
}

type Dog struct {
}

func (d *Dog) Say() {
    fmt.Println("wang wang")
}

//1
var animal Animal
dog := &Dog{}
//2
animal=dog
//3
var e interface{}
e = dog
  1. 初始化定義一個接口變量var animal Animal
iface init
  1. 將實現(xiàn)接口的對象賦值給接口變量animal=dog
iface full
  1. 定義一個空接口變量var e interface{}
empty
  1. 將實現(xiàn)接口的對象賦值給空接口變量e = dog
empty interface

至此勘伺,想必你應該了解了接口的數(shù)據(jù)結(jié)構(gòu)及工作機制跪腹,接下來我們看看反射是如何工作的

反射

反射三大定律

1. Reflection goes from interface value to reflection object 接口數(shù)據(jù)-->反射對象

簡單來說,反射是一種檢查存儲在接口變量中的類型和值的機制飞醉,reflect包定義了這兩個重要的類型TypeValue冲茸,任意接口值在反射中都可以理解為由 reflect.Typereflect.Value兩部分組成,可以通過reflect.TypeOf()reflect.ValueOf()函數(shù)來獲取任意對象的TypeValue缅帘。

func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
reflectTypeValue

舉個栗子

 var x float64 = 3.4
 fmt.Println("type:", reflect.TypeOf(x))

你可能會迷惑,你不是說接口變量才支持反射的嗎轴术?別著急 我們來仔細看看reflect.TypeOfreflect.ValueOf是如何實現(xiàn)的

func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }
    escapes(i)
    return unpackEface(i)
}

很簡單,當我們調(diào)用reflect.TypeOf(x)時钦无,x已經(jīng)存儲進了一個空接口變量逗栽,reflect.TypeOf然后拆箱空接口變量獲取類型信息。

reflect.TypeOfreflect.ValueOf提供了大量的方法可以讓我們檢查和操作它們失暂。

type Type interface {
    // 從內(nèi)存中申請一個類型值時對齊的字節(jié)數(shù).
    Align() int
    // 此類型作為結(jié)構(gòu)體字段時對齊的字節(jié)數(shù)
    FieldAlign() int
  //獲取類型的指定函數(shù)信息
    Method(int) Method
 //通過方法名獲取方法信息
    MethodByName(string) (Method, bool)
    //該類型可導出方法數(shù)量
    NumMethod() int
  // 返回包中定義類型的名稱 為定義類型返回空字符串
    Name() string
    // 返回類型的包路徑即唯一標識包的路徑 如“encoding/base64”
  // 預定義類型彼宠、未定義類型、[]int返回空字符串
    PkgPath() string
  // 返回存儲該類型值需要的字節(jié)數(shù) 類似unsafe.Sizeof
    Size() uintptr
  // 返回該類型的字符串表示形式弟塞。
    String() string
    // 返回類型的特定種類
    Kind() Kind
    // 判斷該類型是否實現(xiàn)了u類型的接口
    Implements(u Type) bool
    // 判斷該類型是否可賦值給u類型
    AssignableTo(u Type) bool
    // 判斷該類型是否可轉(zhuǎn)換為u類型
    ConvertibleTo(u Type) bool
    // 判斷該類型的值是否可比較
    Comparable() bool
    // 方法僅適用于某些類型
    // 取決于具體類型
    //  Int*, Uint*, Float*, Complex*: Bits
    //  Array: Elem, Len
    //  Chan: ChanDir, Elem
    //  Func: In, NumIn, Out, NumOut, IsVariadic.
    //  Map: Key, Elem
    //  Ptr: Elem
    //  Slice: Elem
    //  Struct: Field, FieldByIndex, FieldByName, FieldByNameFunc, NumField

    // 返回類型占用的bit值
    //非 sized or unsized Int, Uint, Float, or Complex 會panic
    Bits() int
    // 返回channel類型 非chan類型panic
    ChanDir() ChanDir
  // 判斷函數(shù)是否有可變參數(shù) 非函數(shù)類型會panic
    IsVariadic() bool
    // 返回元素類型
    // 非 Array, Chan, Map, Ptr, or Slice會panic
    Elem() Type
    // It panics if the type's Kind is not Struct.
    // It panics if i is not in the range [0, NumField()).
  // 返回結(jié)構(gòu)體類型第i個字段
    Field(i int) StructField
    // 等價于Field(i)
    // It panics if the type's Kind is not Struct.
    FieldByIndex(index []int) StructField
    // 根據(jù)名字返回字段信息
    // and a boolean indicating if the field was found.
    FieldByName(name string) (StructField, bool)
  //利用函數(shù)查找字段名符合條件的字段信息 使用廣度優(yōu)先的策略 如果發(fā)現(xiàn)多個匹配則不返回匹配項
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    // It panics if the type's Kind is not Func.
    // It panics if i is not in the range [0, NumIn()).
  // 返回函數(shù)第i個入?yún)?    In(i int) Type
    // It panics if the type's Kind is not Map.
  // 返回map的key類型
    Key() Type
    // It panics if the type's Kind is not Array.
  // 返回數(shù)組類型的長度
    Len() int
    // It panics if the type's Kind is not Struct.
  // 返回結(jié)構(gòu)體類型字段數(shù)量
    NumField() int
    // It panics if the type's Kind is not Func.
  // 返回函數(shù)類型入?yún)?shù)量
    NumIn() int
    // It panics if the type's Kind is not Func.
  // 返回函數(shù)類型出參數(shù)量
    NumOut() int
    // It panics if the type's Kind is not Func.
    // It panics if i is not in the range [0, NumOut()).
  // 返回函數(shù)類型第i個出參
    Out(i int) Type

    common() *rtype
    uncommon() *uncommonType
}

Value

type Value struct {
    // typ 包含由Value表示值的類型
    typ *rtype
    // 指針值數(shù)據(jù)凭峡,如果設置flagIndir則指向數(shù)據(jù)
    // 當設置flagIndir或typ.pointers()為true時有效
    ptr unsafe.Pointer

    // flag保存有關(guān)值的元數(shù)據(jù)
    // 最低位是flag標志位:
    //  - flagStickyRO: 通過未導出未嵌入的字段獲取 故只讀
    //  - flagEmbedRO: 通過未導出嵌入字段獲取故只讀
    //  - flagIndir: val保存指向數(shù)據(jù)的指針
    //  - flagAddr: v.CanAddr 為 true (表示 flagIndir)
    //  - flagMethod: v 為方法值
    // 接下來的5位給出值的類型
    // 重復typ.Kind() 方法值除外.
    // 剩余23+位給方法值的方法編號
    // 如果flag.kind() != Func, 代碼可假定未設置flagMethod
    // 如果ifaceIndir(typ), 代碼可假設設置了flagIndir
    flag
}

2. Reflection goes from reflection object to interface value 反射對象 -->接口數(shù)據(jù)

像物理反射一樣,Go的反射也會生成自己的逆决记。給出一個reflect.Value我們可以使用Interface()方法獲取接口的值摧冀。實際上就是將該類型和值信息打包成接口表示形式并返回。

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

例如:

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
reflect3

當然reflect.Value通過Value.Type()也可以直接獲取reflect.Type

reflect2

3. To modify a reflection object,the value must be settable 若數(shù)據(jù)可修改 可通過反射對象來修改它

我們先來看個栗子:

    var a float64
    fmt.Println(a)
    va := reflect.ValueOf(a)
  va.SetFloat(11) 
    fmt.Println(a)

輸出:

panic: using unaddressable value

為何系宫?看似操作沒問題索昂。其實仔細想想,Go是值傳遞va := reflect.ValueOf(a)中我們相當于傳遞了a的拷貝給了reflect.ValueOf笙瑟,因此即使va.SetFloat(11)修改成功了也無法到達修改a原始值的目的楼镐,故而利用這種Type是否CanSet來避免這種問題。正確做法

  • 首先根據(jù)變量地址獲取reflect.Valueva := reflect.ValueOf(&a)
  • va.SetFloat(11)此時依然無法成功 因為此時的va仍然是一個拷貝值往枷,如若修改需要使用va.Elem()獲取*va
    var a float64
    fmt.Println(a)
    va := reflect.ValueOf(&a)
  va.Elem().SetFloat(11) 
    fmt.Println(a) // 11

反射的應用

反射廣泛應用在對象序列化框产,fmt相關(guān)的函數(shù)以及ORM(Object Relational Mapping)等等

例如:JSON序列化

Go內(nèi)置的Json序列化提供了兩個方法

func Marshal(v interface{}) ([]byte, error) 
func Unmarshal(data []byte, v interface{}) error 

序列化和反序列化參數(shù)中都有interface{}類型的變量凄杯,所以當我們調(diào)用這個函數(shù)時需要使用reflect包中的方法后期參數(shù)的reflect.Valuereflect.Type,進而調(diào)用其get秉宿、set方法戒突。

序列化

func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
    ......

    switch t.Kind() {
    case reflect.Bool:
        return boolEncoder
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        return intEncoder
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
        return uintEncoder
    case reflect.Float32:
        return float32Encoder
    case reflect.Float64:
        return float64Encoder
    case reflect.String:
        return stringEncoder
    case reflect.Interface:
        return interfaceEncoder
    case reflect.Struct:
        return newStructEncoder(t)
    case reflect.Map:
        return newMapEncoder(t)
    case reflect.Slice:
        return newSliceEncoder(t)
    case reflect.Array:
        return newArrayEncoder(t)
    case reflect.Ptr:
        return newPtrEncoder(t)
    default:
        return unsupportedTypeEncoder
    }
}
reflect json

總結(jié)

Go作為靜態(tài)語言,相對于動態(tài)語言描睦,在靈活性上受到某些限制膊存。但是通過reflect包提供類似動態(tài)語言的功能,你可以運行時獲取參數(shù)的ValueType進而完成一些特定的需求忱叭。其轉(zhuǎn)換關(guān)系如圖

reflect4
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末隔崎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子韵丑,更是在濱河造成了極大的恐慌爵卒,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撵彻,死亡現(xiàn)場離奇詭異钓株,居然都是意外死亡,警方通過查閱死者的電腦和手機陌僵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門轴合,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碗短,你說我怎么就攤上這事受葛。” “怎么了偎谁?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵奔坟,是天一觀的道長。 經(jīng)常有香客問我搭盾,道長咳秉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任鸯隅,我火速辦了婚禮澜建,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蝌以。我一直安慰自己炕舵,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布跟畅。 她就那樣靜靜地躺著咽筋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪徊件。 梳的紋絲不亂的頭發(fā)上奸攻,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天蒜危,我揣著相機與錄音,去河邊找鬼睹耐。 笑死辐赞,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的硝训。 我是一名探鬼主播响委,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼窖梁!你這毒婦竟也來了赘风?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤纵刘,失蹤者是張志新(化名)和其女友劉穎贝次,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彰导,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年敲茄,在試婚紗的時候發(fā)現(xiàn)自己被綠了位谋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡堰燎,死狀恐怖掏父,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秆剪,我是刑警寧澤赊淑,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站仅讽,受9級特大地震影響陶缺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜洁灵,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一饱岸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧徽千,春花似錦苫费、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至牍汹,卻和暖如春铐维,著一層夾襖步出監(jiān)牢的瞬間柬泽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工方椎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留聂抢,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓棠众,卻偏偏與公主長得像琳疏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子闸拿,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 各位學習Go語言的朋友空盼,周末好,這次跟大家聊一聊Go語言的一個高級話題:反射新荤。 這篇文章是從我過去的學習筆記修改來...
    大彬_一起學Golang閱讀 1,362評論 2 21
  • 本文轉(zhuǎn)載自https://github.com/KeKe-Li/For-learning-Go-Tutorial/...
    雪域迷影閱讀 343評論 0 0
  • 第一次知道反射的時候還是許多年前在學校里玩 C# 的時候揽趾。那時總是弄不清楚這個復雜的玩意能有什么實際用途……然后發(fā)...
    勿以浮沙筑高臺閱讀 1,124評論 0 9
  • 久違的晴天,家長會苛骨。 家長大會開好到教室時篱瞎,離放學已經(jīng)沒多少時間了。班主任說已經(jīng)安排了三個家長分享經(jīng)驗痒芝。 放學鈴聲...
    飄雪兒5閱讀 7,493評論 16 22
  • 今天感恩節(jié)哎俐筋,感謝一直在我身邊的親朋好友。感恩相遇严衬!感恩不離不棄澄者。 中午開了第一次的黨會,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,551評論 0 11