Golang 反射的使用

聲明: 轉(zhuǎn)載自golang reflect包,反射學(xué)習(xí)與實踐

Go 語言反射的三大法則罢坝,其中包括:

  • 從 interface{} 變量可以反射出反射對象廓握;
  • 從反射對象可以獲取 interface{} 變量;
  • 要修改反射對象嘁酿,其值必須可設(shè)置隙券;

從反射對象到接口值的過程就是從接口值到反射對象的鏡面過程,兩個過程都需要經(jīng)歷兩次轉(zhuǎn)換:

  • 從接口值到反射對象:
    • 從基本類型到接口類型的類型轉(zhuǎn)換痹仙;
    • 從接口類型到反射對象的轉(zhuǎn)換是尔;
  • 從反射對象到接口值:
    • 反射對象轉(zhuǎn)換成接口類型;
    • 通過顯式類型轉(zhuǎn)換變成原始類型开仰;

Type拟枚,Value

反射包中的所有方法基本都是圍繞著TypeValue這兩個類型設(shè)計的薪铜。我們通過reflect.TypeOfreflect.ValueOf可以將一個普通的變量轉(zhuǎn)換成『反射』包中提供的TypeValue恩溅,隨后就可以使用反射包中的方法對它們進行復(fù)雜的操作隔箍。

類型Type是反射包定義的一個接口,我們可以使用 reflect.TypeOf 函數(shù)獲取任意變量的的類型脚乡,Type 接口中定義了一些有趣的方法蜒滩,MethodByName 可以獲取當(dāng)前類型對應(yīng)方法的引用、Implements 可以判斷當(dāng)前類型是否實現(xiàn)了某個接口:

type Type interface {
    // 變量的內(nèi)存對齊奶稠,返回 rtype.align
    Align() int

    // struct 字段的內(nèi)存對齊俯艰,返回 rtype.fieldAlign
    FieldAlign() int

    // 根據(jù)傳入的 i,返回方法實例锌订,表示類型的第 i 個方法
    Method(int) Method

    // 根據(jù)名字返回方法實例竹握,這個比較常用
    MethodByName(string) (Method, bool)

    // 返回類型方法集中可導(dǎo)出的方法的數(shù)量
    NumMethod() int

    // 只返回類型名,不含包名
    Name() string

    // 返回導(dǎo)入路徑辆飘,即 import 路徑
    PkgPath() string

    // 返回 rtype.size 即類型大小啦辐,單位是字節(jié)數(shù)
    Size() uintptr

    // 返回類型名字,實際就是 PkgPath() + Name()
    String() string

    // 返回 rtype.kind蜈项,描述一種基礎(chǔ)類型
    Kind() Kind

    // 檢查當(dāng)前類型有沒有實現(xiàn)接口 u
    Implements(u Type) bool

    // 檢查當(dāng)前類型能不能賦值給接口 u
    AssignableTo(u Type) bool

    // 檢查當(dāng)前類型能不能轉(zhuǎn)換成接口 u 類型
    ConvertibleTo(u Type) bool

    // 檢查當(dāng)前類型能不能做比較運算芹关,其實就是看這個類型底層有沒有綁定 typeAlg 的 equal 方法。
    // 打捉糇洹侥衬!不要去搜 typeAlg 是什么,不然你會陷進去的常侦!先把本文看完浇冰。
    Comparable() bool

    // 返回類型的位大小,但不是所有類型都能調(diào)這個方法聋亡,不能調(diào)的會 panic
    Bits() int

    // 返回 channel 類型的方向肘习,如果不是 channel,會 panic
    ChanDir() ChanDir

    // 返回函數(shù)類型的最后一個參數(shù)是不是可變數(shù)量的坡倔,"..." 就這樣的漂佩,同樣,如果不是函數(shù)類型罪塔,會 panic
    IsVariadic() bool

    // 返回所包含元素的類型投蝉,只有 Array, Chan, Map, Ptr, Slice 這些才能調(diào),其他類型會 panic征堪。
    // 這不是廢話嗎瘩缆。。其他類型也沒有包含元素一說佃蚜。
    Elem() Type

    // 返回 struct 類型的第 i 個字段庸娱,不是 struct 會 panic着绊,i 越界也會 panic
    Field(i int) StructField

    // 跟上邊一樣,不過是嵌套調(diào)用的熟尉,比如 [1, 2] 就是說返回當(dāng)前 struct 的第1個struct 的第2個字段归露,適用于 struct 本身嵌套的類型
    FieldByIndex(index []int) StructField

    // 按名字找 struct 字段,第二個返回值 ok 表示有沒有
    FieldByName(name string) (StructField, bool)

    // 按函數(shù)名找 struct 字段斤儿,因為 struct 里也可能有類型是 func 的嘛
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    
    // 返回函數(shù)第 i 個參數(shù)的類型剧包,不是 func 會 panic
    In(i int) Type

    // 返回 map 的 key 的類型,不是 map 會 panic
    Key() Type

    // 返回 array 的長度往果,不是 array 會 panic
    Len() int

    // 返回 struct 字段數(shù)量疆液,不是 struct 會 panic
    NumField() int

    // 返回函數(shù)的參數(shù)數(shù)量,不是 func 會 panic
    NumIn() int

    // 返回函數(shù)的返回值數(shù)量棚放,不是 func 會 panic
    NumOut() int

    // 返回函數(shù)第 i 個返回值的類型枚粘,不是 func 會 panic
    Out(i int) Type
}

反射包中 Value 的類型與 Type 不同,它被聲明成了結(jié)構(gòu)體飘蚯。這個結(jié)構(gòu)體沒有對外暴露的字段,但是提供了獲取或者寫入數(shù)據(jù)的方法:

type Value struct {
    // 反射出來此值的類型福也,rtype 是啥往上看局骤,但可別弄錯了,這 typ 是未導(dǎo)出的暴凑,從外部調(diào)不到 Type 接口的方法
    typ *rtype

    // 數(shù)據(jù)形式的指針值
    ptr unsafe.Pointer

    // 保存元數(shù)據(jù)
    flag
}
// 前提 v 是一個 func峦甩,然后調(diào)用 v,并傳入 in 參數(shù)现喳,第一個參數(shù)是 in[0]凯傲,第二個是 in[1],以此類推
func (v Value) Call(in []Value) []Value

// 返回 v 的接口值或者指針
func (v Value) Elem() Value

// 前提 v 是一個 struct嗦篱,返回第 i 個字段冰单,這個主要用于遍歷
func (v Value) Field(i int) Value

// 前提 v 是一個 struct,根據(jù)字段名直接定位返回
func (v Value) FieldByName(name string) Value

// 前提 v 是 Array, Slice, String 之一灸促,返回第 i 個元素诫欠,主要也是用于遍歷,注意不能越界
func (v Value) Index(i int) Value

// 判斷 v 是不是 nil浴栽,只有 chan, func, interface, map, pointer, slice 可以用荒叼,其他類型會 panic
func (v Value) IsNil() bool

// 判斷 v 是否合法,如果返回 false典鸡,那么除了 String() 以外的其他方法調(diào)用都會 panic被廓,事前檢查是必要的
func (v Value) IsValid() bool

// 前提 v 是個 map,返回對應(yīng) value
func (v Value) MapIndex(key Value)

// 前提 v 是個 map萝玷,返回所有 key 組成的一個 slice
func (v Value) MapKeys() []Value

// 前提 v 是個 struct嫁乘,返回字段個數(shù)
func (v Value) NumField() int

// 賦值
func (v Value) Set(x Value)

// 類型
func (v Value) Type() Type

// 等等...

實踐

  • 遍歷一個結(jié)構(gòu)體的字段以及對應(yīng)的值

package main

import (
      "fmt"
      "reflect"
  )

type Person struct {
      Name     string
      Sex      string
      Age      int
      PhoneNum string
      School   string
      City     string
  }

func main() {
      p1 := Person{
          Name:     "tom",
          Sex:      "male",
          Age:      10,
          PhoneNum: "1000000",
          School:   "spb-kindergarden",
          City:     "cq",
      }

      rv := reflect.ValueOf(p1)
      rt := reflect.TypeOf(p1)
      if rv.Kind() == reflect.Struct {
          for i := 0; i < rt.NumField(); i++ {
              //按順序遍歷
              fmt.Printf("field:%+v,value:%+v\n", rt.Field(i).Name, rv.Field(i))
          }
      }
  }
  • 若知道字段名英遭,直接去取該字段

rv := reflect.ValueOf(p1)
rt := reflect.TypeOf(p1)
//可以直接取想要的字段
//reflect的type interface,F(xiàn)ieldByName方法會返回字段信息以及是否有該字段亦渗;
if f, ok := rt.FieldByName("Age"); ok {
    fmt.Printf("field:%+v,value:%+v\n", f.Name, rv.FieldByName("Age"))
}

字段信息是一個結(jié)構(gòu)體挖诸,它描述了該字段的下列屬性:

// A StructField describes a single field in a struct.
  type StructField struct {
      // Name is the field name.
      Name string
      // PkgPath is the package path that qualifies a lower case (unexported)
      // field name. It is empty for upper case (exported) field names.
      // See https://golang.org/ref/spec#Uniqueness_of_identifiers
      PkgPath string

      Type      Type      // field type
      Tag       StructTag // field tag string
      Offset    uintptr   // offset within struct, in bytes
      Index     []int     // index sequence for Type.FieldByIndex
      Anonymous bool      // is an embedded field
  }
  • 判斷一個變量的類型

rv := reflect.ValueOf(p1)
rt := reflect.TypeOf(p1)
fmt.Printf("kind is %+v\n", rt.Kind())
fmt.Printf("kind is %+v\n", rv.Kind())

typevalueKind()方法都可以返回該變量的類型,不過若取得value后發(fā)現(xiàn)其是一個零值法精,那么會返回KindInvalid

  // Kind returns v's Kind.
  // If v is the zero Value (IsValid returns false),        Kind returns Invalid.
  func (v Value) Kind() Kind {
      return v.kind()
  }

reflectKind一共有27種類型多律,基本攬括了所有g(shù)olang中的類型

const (
      Invalid Kind = iota
      Bool
      Int
      Int8
      Int16
      Int32
      Int64
      Uint
      Uint8
      Uint16
      Uint32
      Uint64
      Uintptr
      Float32
      Float64
      Complex64
      Complex128
      Array
      Chan
      Func
      Interface
      Map
      Ptr
      Slice
      String
      Struct
      UnsafePointer
  )
  • 獲取tag的值

type TagTest struct {
    Name string `json:"name_json"`
    Age  int    `json:"age_json"`
}

t := TagTest{Name: "tom", Age: 10}
rtt := reflect.TypeOf(t)
//rtv := reflect.ValueOf(t)
for i := 0; i < rtt.NumField(); i++ {
    field := rtt.Field(i)
    if json, ok := field.Tag.Lookup("json"); ok {
        fmt.Printf("tag is %+v, value is %+v\n", json, field.Tag.Get("json"))
    }
}

注意,field.Tag.Lookup()field.Tag.Get()方法都是取tag的值搂蜓,只不過Lookup會用第二個返回值返回是否存在這個tag狼荞,而Get方法若不存在這個tag會返回一個空字符串

  • 動態(tài)調(diào)用方法

*T有方法Add

  type T struct{}

  func (t *T) Add(a, b int) {
      fmt.Printf("a + b is %+v\n", a+b)
  }

動態(tài)調(diào)用

funcName := "Add"
typeT := &T{}
a := reflect.ValueOf(1)
b := reflect.ValueOf(2)
in := []reflect.Value{a, b}
reflect.ValueOf(typeT).MethodByName(funcName).Call(in)
  • 動態(tài)調(diào)用含返回值的方法

  func (t *T) AddRetErr(a, b int) (int, error) {
      if a+b < 10 {
          return a + b, errors.New("total lt 10")
      }
      return a + b, nil
  }

調(diào)用

      funcName = "AddRetErr"
      ret := reflect.ValueOf(typeT).MethodByName(funcName).Call(in)
      fmt.Printf("ret is %+v\n", ret)
      for i := 0; i < len(ret); i++ {
          fmt.Printf("ret index:%+v, type:%+v, value:%+v\n", i, ret[i].Kind(), ret[i].Interface())
      }

這里的ret[i].Kind(),若非基礎(chǔ)類型帮碰,會得到interface

如果err不是nil相味,

      if v, ok := ret[1].Interface().(error); ok {
          fmt.Printf("v is %+v\n", v)
      }

類型斷言會成功,可以用這種方式去判斷返回的error是否為空

  • 通過反射修改值

不是所有的反射值都可以修改殉挽。對于一個反射值是否可以修改丰涉,可以通過CanSet()進行檢查。

要修改值斯碌,必須滿足:

  • 可以尋址
  • 可尋址的類型:
    • 指針指向的具體元素
    • slice的元素
    • 結(jié)構(gòu)體指針的字段
    • 數(shù)組指針的元素
1. 指針指向的具體元素

需要三步:

  1. 取地址:v := reflect.ValueOf(&x)
  2. 判斷v.Elem()是否可以設(shè)值
  3. 給v.Elem()設(shè)置具體值
      ta := 10
      vta := reflect.ValueOf(&ta)
      if vta.Elem().CanSet() {
          vta.Elem().Set(reflect.ValueOf(11))
      }
      fmt.Println("cant set")
      fmt.Printf("vta is :%+v\n", vta.Elem())
2. slice中的元素
      ts := []int{1, 2, 3}
      tsV := reflect.ValueOf(ts)
      if tsV.Index(0).CanSet() {
          tsV.Index(0).Set(reflect.ValueOf(10))
      }
      fmt.Printf("ts is %+v\n", ts)
      //輸出:ts is [10 2 3]
3. 結(jié)構(gòu)體指針的字段
      t1 := TagTest{}
      tV := reflect.ValueOf(t)
      
      //結(jié)構(gòu)體指針
      t1V := reflect.ValueOf(&t1)
      
      fmt.Printf("tV:%+v\n", tV)
      for i := 0; i < tV.NumField(); i++ {
          val := tV.Field(i)
          t1V.Elem().Field(i).Set(val)
      }
      fmt.Printf("t1 is %+v\n", t1)
4. 數(shù)組指針的元素
      tsA := [3]int{1, 2, 3}
      tsAv := reflect.ValueOf(&tsA)
      if tsAv.Elem().Index(0).CanSet() {
          tsAv.Elem().Index(0).Set(reflect.ValueOf(10))
      }
      fmt.Printf("tsA is %+v\n", tsA)

注意與slice中元素修改的區(qū)別

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末一死,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子傻唾,更是在濱河造成了極大的恐慌投慈,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冠骄,死亡現(xiàn)場離奇詭異伪煤,居然都是意外死亡,警方通過查閱死者的電腦和手機凛辣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門抱既,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蟀给,你說我怎么就攤上這事蝙砌。” “怎么了跋理?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵择克,是天一觀的道長。 經(jīng)常有香客問我前普,道長肚邢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮骡湖,結(jié)果婚禮上贱纠,老公的妹妹穿的比我還像新娘。我一直安慰自己响蕴,他們只是感情好谆焊,可當(dāng)我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浦夷,像睡著了一般辖试。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上劈狐,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天罐孝,我揣著相機與錄音,去河邊找鬼肥缔。 笑死莲兢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的续膳。 我是一名探鬼主播改艇,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼姑宽!你這毒婦竟也來了遣耍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤炮车,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后酣溃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘦穆,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年赊豌,在試婚紗的時候發(fā)現(xiàn)自己被綠了扛或。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡碘饼,死狀恐怖熙兔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艾恼,我是刑警寧澤住涉,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站钠绍,受9級特大地震影響舆声,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一媳握、第九天 我趴在偏房一處隱蔽的房頂上張望碱屁。 院中可真熱鬧,春花似錦蛾找、人聲如沸娩脾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柿赊。三九已至,卻和暖如春隘冲,著一層夾襖步出監(jiān)牢的瞬間闹瞧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工展辞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留奥邮,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓罗珍,卻偏偏與公主長得像洽腺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子覆旱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,562評論 2 349

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