手摸手Go 接口与反射

Go是强类型/静态类型语言,每个变量在编译时就已经确定是哪种静态类型。反射(reflection)是程序在运行时可以访问、检测、修改自身状态或行为的一种能力。在Java出现后迅速流行起来的概念,Go也提供了这种在运行时更新、检查变量值、调用变量的方法和变量支持的内在操作的机制,一定程度上弥补了静态语言在动态行为上的不足。

正常来讲,程序在编译时会将变量转换为内存地址,变量名不会被编译器写入可执行部分,那么运行时程序就无法获取自身的信息。支持反射的语言则需要在程序编译期将变量的反射信息,如字段名、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息。这样程序运行时即可获取类型的反射信息,并有能力操作修改它。

反射是把双刃剑,虽然代码更加灵活了但是

  • 代码阅读起来也困难了
  • 一定程度上破坏了静态类型语言的编译期检查 运行时会有panic风险
  • 降低了系统性能

我们为什么需要反射?

  1. 无法预定义参数类型
  2. 函数需要根据入参来动态执行

需要注意的是:Go中只有接口类型才可以反射,而反射又是建立在类型系统之上,so我们先来复习下类型与接口的知识

类型

Go是静态类型语言。每个变量都有一个静态类型,编译时就已经确定的类型:int、float32、*MyType、[]byte等等

type MyInt int

var i int
var j MyInt

上面的栗子中,i与j具有不同的静态类型(i是int类型,j为MyInt类型),尽管他们的基础类型都是int,但是他们之间不经过转换无法相互赋值。

类型的一个重要类别是接口类型,接口可以存储任何非接口的具体值,只要该值实现了接口方法即可。

接口

接口是多个方法声明的集合,侧重于做什么,不关系怎么做 谁来做。它更像是一种调用契约或协议(protocol)。接口解除了类型依赖,屏蔽了方法实现细节,但接口的实现机制存在运行时开销。

Go的接口机制比较简洁,不像Java需要显示声明实现的接口,Go只要目标类型方法集中包含了接口声明的全部方法,就被称为实现了该接口,无须显示声明。

如果一个接口没有声明任何方法,那么就是一个空接口interface{},类似JavaObject对象可以被赋值为任意类型的对象。但

Go语言的接口类型不是任意类型 只是任意类型可以通过类型转换成接口变量

接下来我们来看看接口的数据结构,总结起来接口结构如下:

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数据类型

type _type struct {
   size       uintptr //类型大小
   ptrdata    uintptr // 含有所有指针类型前缀大小
   hash       uint32 //类型hash值 避免在哈希表中计算
   tflag      tflag //额外类型信息标志
   align      uint8 // 类型变量对齐方式
   fieldalign uint8 // 类型结构字段对齐方式
   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组合其他基础类型而成

接下来我们通过一个栗子拆解下接口内存中的结构究竟如何

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. 将实现接口的对象赋值给接口变量animal=dog
iface full
  1. 定义一个空接口变量var e interface{}
empty
  1. 将实现接口的对象赋值给空接口变量e = dog
empty interface

至此,想必你应该了解了接口的数据结构及工作机制,接下来我们看看反射是如何工作的

反射

反射三大定律

1. Reflection goes from interface value to reflection object 接口数据-->反射对象

简单来说,反射是一种检查存储在接口变量中的类型和值的机制,reflect包定义了这两个重要的类型TypeValue,任意接口值在反射中都可以理解为由 reflect.Typereflect.Value两部分组成,可以通过reflect.TypeOf()reflect.ValueOf()函数来获取任意对象的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是如何实现的

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)
}

很简单,当我们调用reflect.TypeOf(x)时,x已经存储进了一个空接口变量,reflect.TypeOf然后拆箱空接口变量获取类型信息。

reflect.TypeOfreflect.ValueOf提供了大量的方法可以让我们检查和操作它们。

type Type interface {
    // 从内存中申请一个类型值时对齐的字节数.
    Align() int
    // 此类型作为结构体字段时对齐的字节数
    FieldAlign() int
  //获取类型的指定函数信息
    Method(int) Method
 //通过方法名获取方法信息
    MethodByName(string) (Method, bool)
    //该类型可导出方法数量
    NumMethod() int
  // 返回包中定义类型的名称 为定义类型返回空字符串
    Name() string
    // 返回类型的包路径即唯一标识包的路径 如“encoding/base64”
  // 预定义类型、未定义类型、[]int返回空字符串
    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
    // 方法仅适用于某些类型
    // 取决于具体类型
    //  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
  // 判断函数是否有可变参数 非函数类型会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()).
  // 返回结构体类型第i个字段
    Field(i int) StructField
    // 等价于Field(i)
    // It panics if the type's Kind is not Struct.
    FieldByIndex(index []int) StructField
    // 根据名字返回字段信息
    // and a boolean indicating if the field was found.
    FieldByName(name string) (StructField, bool)
  //利用函数查找字段名符合条件的字段信息 使用广度优先的策略 如果发现多个匹配则不返回匹配项
    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()).
  // 返回函数第i个入参
    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.
  // 返回数组类型的长度
    Len() int
    // It panics if the type's Kind is not Struct.
  // 返回结构体类型字段数量
    NumField() int
    // It panics if the type's Kind is not Func.
  // 返回函数类型入参数量
    NumIn() int
    // It panics if the type's Kind is not Func.
  // 返回函数类型出参数量
    NumOut() int
    // It panics if the type's Kind is not Func.
    // It panics if i is not in the range [0, NumOut()).
  // 返回函数类型第i个出参
    Out(i int) Type

    common() *rtype
    uncommon() *uncommonType
}

Value

type Value struct {
    // typ 包含由Value表示值的类型
    typ *rtype
    // 指针值数据,如果设置flagIndir则指向数据
    // 当设置flagIndir或typ.pointers()为true时有效
    ptr unsafe.Pointer

    // flag保存有关值的元数据
    // 最低位是flag标志位:
    //  - flagStickyRO: 通过未导出未嵌入的字段获取 故只读
    //  - flagEmbedRO: 通过未导出嵌入字段获取故只读
    //  - flagIndir: val保存指向数据的指针
    //  - 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 反射对象 -->接口数据

像物理反射一样,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 若数据可修改 可通过反射对象来修改它

我们先来看个栗子:

    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来避免这种问题。正确做法

  • 首先根据变量地址获取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相关的函数以及ORM(Object Relational Mapping)等等

例如:JSON序列化

Go内置的Json序列化提供了两个方法

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

序列化和反序列化参数中都有interface{}类型的变量,所以当我们调用这个函数时需要使用reflect包中的方法后期参数的reflect.Valuereflect.Type,进而调用其getset方法。

序列化

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

总结

Go作为静态语言,相对于动态语言,在灵活性上受到某些限制。但是通过reflect包提供类似动态语言的功能,你可以运行时获取参数的ValueType进而完成一些特定的需求。其转换关系如图

reflect4
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容

  • 各位学习Go语言的朋友,周末好,这次跟大家聊一聊Go语言的一个高级话题:反射。 这篇文章是从我过去的学习笔记修改来...
    大彬_一起学Golang阅读 1,365评论 2 21
  • 本文转载自https://github.com/KeKe-Li/For-learning-Go-Tutorial/...
    雪域迷影阅读 355评论 0 0
  • 第一次知道反射的时候还是许多年前在学校里玩 C# 的时候。那时总是弄不清楚这个复杂的玩意能有什么实际用途……然后发...
    勿以浮沙筑高台阅读 1,125评论 0 9
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,515评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,561评论 0 11