Golang反射模型

序言

第一次接触反射技术是在很多年前学习设计模式的时候,那时在优化Java版简单工厂的实现,当读取配置信息中的的类型字符串后利用反射来创建对象实例,替代了switch case语句的分支判断。
第二次接触反射技术是在几年前微服务架构开始大范围流行的时候,那时在考虑异构微服务的多版本集成问题,支持反射的语言(Java等)的序列化就容易很多,而不支持反射的语言(C++等)的序列化就麻烦一些,不过最后统一选择了序列化工具protobuf,支持跨语言。
第三次接触反射技术是在听某个团队Session的时候,那时在简单分享Golang的反射机制,发现很多同学都没什么感觉,主要原因是大家在产品代码中几乎不会使用到反射技术。

在生活中,反射像一面镜子,通过镜子你可以清楚的看见自己的一切。


reflection.png

在计算机中,反射表示程序能够检查自身结构的一种能力,尤其是类型。通过反射,你可以获取对象类型的详细信息,并可动态操作对象。

众所周知,大多数语言都支持反射,但每种语言的反射模型却不尽相同。笔者在这一年多的Golang实践中,学习或二次开发了很多个框架,其中大部分都涉及反射技术,比如GoConvey,GoMock,GoStub,Monkey和gRPC等,所以本文的主角是Golang反射模型

反射应用的一个简单案例

我们在讨论反射模型之前,先看看反射应用的一个简单案例:

// Stubs represents a set of stubbed variables that can be reset.
type Stubs struct {
    // stubs is a map from the variable pointer (being stubbed) to the original value.
    stubs   map[reflect.Value]reflect.Value
    origEnv map[string]envVal
}

// Stub replaces the value stored at varToStub with stubVal.
// varToStub must be a pointer to the variable. stubVal should have a type
// that is assignable to the variable.
func (s *Stubs) Stub(varToStub interface{}, stubVal interface{}) *Stubs {
    v := reflect.ValueOf(varToStub)
    stub := reflect.ValueOf(stubVal)

    // Ensure varToStub is a pointer to the variable.
    if v.Type().Kind() != reflect.Ptr {
        panic("variable to stub is expected to be a pointer")
    }

    if _, ok := s.stubs[v]; !ok {
        // Store the original value if this is the first time varPtr is being stubbed.
        s.stubs[v] = reflect.ValueOf(v.Elem().Interface())
    }

    // *varToStub = stubVal
    v.Elem().Set(stub)
    return s
}

// Reset resets all stubbed variables back to their original values.
func (s *Stubs) Reset() {
    for v, originalVal := range s.stubs {
        v.Elem().Set(originalVal)
    }
    s.resetEnv()
}

上面的代码来自GoStub框架,Stub方法可以连续对多个全局变量或函数变量打桩,Reset方法对所有全局变量或函数变量的桩进行回滚。
Stub方法有两个参数,第一个参数是变量的地址,第二个参数是变量的桩。这两个参数的类型都是空接口,在方法处理的开始都转换成了reflect.Value类型。然而,无论怎么转换,第一个参数必须是指针,因为最终目标是要改变指针指向的值,所以要进行“址传递”。当指针校验不通过时(v.Type().Kind() != reflect.Ptr),就会触发panic。Stub方法先在map中保存变量指针和变量初始值,然后修改变量指针指向的值为桩。Reset方法将map中保存的所有变量指针指向的值修改为初始值。

这就是反射应用的一个简单案例。大家先了解大体意思就行,不用太纠结技术细节,等理解反射模型后,就会发现it is a piece of cake

因为反射建立在类型系统之上,所以我们先从基础知识开始:)

类型系统

顾名思义,类型系统是指一个语言的类型体系结构,是一门编程语言的地基。一个典型的类型系统统筹包括如下几本内容:

  • 基本类型,如int,bool,float,string等
  • 复合类型,如数组,结构体,指针等
  • Any类型
  • 值语义和引用语义
  • 接口

Golang与Python不同,变量类型是静态的,即变量在创建的时候类型就已经确定。
我们对变量做如下声明:

type MyInt int

var i int
var j MyInt

变量i的类型是int,而变量j的类型是MyInt,虽然底层类型都是int,但是它们的静态类型并不一样,当不经过类型转换直接相互赋值时,编译器会报错。

Golang中的指针相对C/C++语言进行了大量的简化,没有”强大“的指针计算功能,仅仅代表变量的地址,属于复合类型。当指针类型作为函数参数类型时,为“址传递”。
Golang中有四种类型比较特殊,为引用类型,分别为slice,map,channel和interface。当引用类型作为函数参数类型时,为“引用传递”,只能使modify操作生效,而add操作不能生效。要想使add操作生效,必须使用“址传递”,所以json.Unmarshal时引用类型的变量也要进行“址传递”。反过来说,如果没有add操作,则就没有必要进行“址传递”,“引用传递”的效率也很高。
其他类型作为函数参数类型时均为“值传递”。

在Golang中,可以给任意自定义类型添加相应的方法,指针类型除外。

type Integer int
func (a Integer) Less(b Integer) bool { 
    return a < b
 }

Integer和int属于不同的静态类型,更重要的是Integer可以像普通的类一样使用:

func main() {
    var a Integer = 1 
    if a.Less(2) {
        fmt.Println(a, "Less 2")
    }
}

Golang中的struct和其它语言中的class有同等重要的地位,但是Golang放弃了包括继承在内的大量面向对象特性,通过组合来解决所有问题(通过匿名组合来模拟继承的一部分功能)。

Golang中的interface是整个类型系统的基石:

  • 非侵入式:一个类只要实现了接口要求的所有函数,就说这个类实现了该接口
  • 接口赋值:既可以将对象实例赋值给接口,也可以将一个接口赋值给另一个接口
  • 接口查询:可以直接了当地询问接口指向的对象实例是否实现了另一个接口
  • 类型查询:可以直接了当地询问接口指向的对象实例的类型
  • 接口组合:可以认为接口组合式类型匿名组合的一个特殊场景
  • Any类型:任意类型都实现了零个或多个方法,那就是说任意类型都实现了空接口interface{},即interface{}是可以指向任意对象的Any类型

反射模型

在Golang的实现中,每个interface变量都有一个对应的pair,pair中记录了实际变量的值和类型,即(value, type)。在基本的层面上,反射只是一个检查存储在interface变量中的value和type的算法,value和type用类型reflect.Value和reflect.Type描述。
尽管从reflect.Value也很容易得到reflect.Type(reflect.Value.Type()),但为了让value和type在概念上进行分离,我们更习惯用pair进行表达。

经过学习和实践,我们将Golang中的反射模型形式化表达如下:

interface --> (value, type)
(value, type) --> (value, kind)
(value, type) --> interface
(value, type) --> (value1, type)

interface --> (value, type)

定义类File:

type File struct { 
    ...
}
func (f *File) Read(buf []byte) (n int, err error) {
    ...
}

func (f *File) Write(buf []byte) (n int, err error) {
    ...
}

func (f *File) Seek(off int64, whence int) (pos int64, err error) {
    ...
}

func (f *File) Close() error {
    ...
}

定义接口Reader,Writer和Closer:

type Reader interface {
    Read(buf []byte) (n int, err error)
}

type Writer interface {
    Write(buf []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类却实现了这些接口:

f := &File{}
var r Reader
r = f
var w Writer
w = r.(Writer)
var c Closer
c = w.(Closer)

空接口可以直接赋值:

var e interface{}
e = c

上面代码中,r,w,c和e的pair均为(f, *File)。pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。

如何从接口变量中获取value和type信息?
通过reflect.ValueOf函数获取value,通过reflect.TypeOf函数获取type:

package main

import (
    "fmt"
    "reflect"
)

type MyInt int

func main() {
    var i MyInt = 10
    fmt.Println("value: ", reflect.ValueOf(i))
    fmt.Println("type: ", reflect.TypeOf(i))
}

运行时输出的结果是:

value:  10
type:  main.MyInt

可见type中描述的是静态类型。

(value, type) --> (value, kind)

(value, type)中的type描述的是静态类型,那如何才能知道底层类型?

reflect.Type有一个Kind方法,而通过Kind方法返回一个常量来表示底层类型:

package main

import (
    "fmt"
    "reflect"
)

type MyInt int

func main() {
    var i MyInt = 10
    fmt.Println("type: ", reflect.TypeOf(i))
    fmt.Println("type.Kind: ", reflect.TypeOf(i).Kind())
}

运行时输出的结果是:

type:  main.MyInt
type.Kind:  int

底层类型在reflect包中有详细的定义:

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

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

(value, type) --> interface

reflect.Value可以使用Interface方法还原接口值,该方法高效的打包类型和值信息到接口值中,并返回这个结果:

func (v Value) Interface() (i interface{}) {
    return valueInterface(v, true)
}

当得到一个类型为reflect.Value的变量,可以通过下面的方式转换变量的类型:

t := v.Interface().(T)

当类型查询成功后,t就可以使用T的成员和方法。

reflect.Value除过能直接转换成Interface{},还可以直接转换成基本类型,比如:

func (v Value) Bool() bool
func (v Value) Bytes() []byte
func (v Value) Int() int64
func (v Value) Float() float64
...

以Int方法为例,如果v.Kind()不是Int, Int8, Int16, Int32, 活着Int64,会触发panic。

(value, type) --> (value1, type)

reflect.Value变量是通过reflect.Value(n)获得的,n可能是值也可能是指针。

当n是值时,不能通过reflect.Value修改n的值:

n := 3.4
v := reflect.ValueOf(n)
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(4.1)

运行上面的代码,结果如下:

settability of v: false
panic: reflect: reflect.Value.SetFloat using unaddressable value

当n是指针时,可以通过reflect.Value修改n指向的变量的值:

m := 3.4
n := &m
p := reflect.ValueOf(n)
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
v.SetFloat(4.1)
fmt.Println("m: ", m)

运行上面的代码,结果如下:

settability of v: true
m:  4.1

进阶话题

对结构的反射操作

对于结构的反射操作并没有根本的不同,只是用了reflect.Value和reflect.Type的Field方法按索引获取对应的成员,比如:

type Student struct {
    Id   int
    Name string
}

s := Student{100032, "zxl"}
v := reflect.ValueOf(s)
t := v.Type()
for i := 0; i < v.NumField(); i++ {
    f := v.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        t.Field(i).Name, f.Type(), f.Interface())
}

运行上面的代码,结果如下:

0: Id int = 100032
1: Name string = zxl

如果想修改结构体中成员的值时,则

  • v.CanSet()必须为true
  • 该成员名必须大写(可导出)
type Student struct {
    Id   int
    Name string
}

s := Student{100032, "zxl"}
v := reflect.ValueOf(&s).Elem()
t := v.Type()
v.Field(0).SetInt(230001)
v.Field(1).SetString("lxz")
for i := 0; i < v.NumField(); i++ {
    f := v.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        t.Field(i).Name, f.Type(), f.Interface())
}

运行上面的代码,结果如下:

0: Id int = 230001
1: Name string = lxz

通过反射创建一个匿名函数

假设函数FuncReturing的功能是创建一个匿名函数:

func FuncReturning(funcType reflect.Type, results ...interface{}) reflect.Value {
    var resultValues []reflect.Value
    for i, r := range results {
        var retValue reflect.Value
        if r == nil {
            retValue = reflect.Zero(funcType.Out(i))
        } else {
            tempV := reflect.New(funcType.Out(i))
            tempV.Elem().Set(reflect.ValueOf(r))
            retValue = tempV.Elem()
        }
        resultValues = append(resultValues, retValue)
    }
    return reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value {
        return resultValues
    })
}

函数FuncReturing的第一个参数funcType用于描述匿名函数的类型,其余参数results是一个变参(本质是数组切片的语法糖),用于匿名函数的返回值的输入:

  • 当results中的元素r等于nil时,我们不能直接使用reflect.ValueOf(nil),而是生成一个reflect.Value零值
  • 当results中的元素r不等于nil时,我们不能直接使用reflect.ValueOf(r),而是先使用reflect.New生成一个reflect.Value对象,然后设置这个对象的数据

最后通过reflect.MakeFunc生成一个funcType类型的匿名函数。

注:为了简单起见,例子中的匿名函数只关注返回值,而对参数不care。

通过反射调用函数或方法

在反射中,函数或方法的底层类型都是reflect.Func。如果要调用函数或方法的话,可以使用reflect.Value的Call方法,参数和返回值类型都是[]reflect.Value。

通过反射调用函数

示例代码:

func prints(i int) string {
    fmt.Println("i =", i)
    return strconv.Itoa(i)
}

func main() {
    v := reflect.ValueOf(prints)
    params := make([]reflect.Value, 1)
    params[0] = reflect.ValueOf(20)
    fmt.Println("result:", v.Call(params)[0])
}

运行上面的代码,结果如下:

i = 20
result: 20

通过反射调用方法

方法和函数可以说在本质上是相同的,只不过方法与一个对象进行了绑定,方法是对象的一种行为,比如:

type Student struct {
    id   int
    name string
}

func (s *Student) SetId(id int) {
    s.id = id
}

func (s *Student) SetName(name string) {
    s.name = name
}

func (s *Student) String() string {
    return "id = " + strconv.Itoa(s.id) + ", " + "name = " + s.name
}

使用reflect.Value的MethodByName方法:

func main() {
    s := &Student{100032, "zxl"}
    v := reflect.ValueOf(s)
    fmt.Println("Before:", v.MethodByName("String").Call(nil)[0])

    params := make([]reflect.Value, 1)
    params[0] = reflect.ValueOf(230001)
    v.MethodByName("SetId").Call(params)

    params[0] = reflect.ValueOf("lxz")
    v.MethodByName("SetName").Call(params)

    fmt.Println("After:", v.MethodByName("String").Call(nil)[0])
}

运行上面的代码,结果如下:

Before: id = 100032, name = zxl
After: id = 230001, name = lxz

使用reflect.Value的Method方法:

func main() {
    s := &Student{100032, "zxl"}
    v := reflect.ValueOf(s)
    fmt.Println("Before:", v.Method(2).Call(nil)[0])

    params := make([]reflect.Value, 1)
    params[0] = reflect.ValueOf(230001)
    v.Method(0).Call(params)

    params[0] = reflect.ValueOf("lxz")
    v.Method(1).Call(params)

    fmt.Println("After:", v.Method(2).Call(nil)[0])
}

运行上面的代码,结果如下:

Before: id = 100032, name = zxl
After: id = 230001, name = lxz

小结

本文先通过一个简单案例来说明Golang反射技术的价值,接着简单回顾了类型系统,然后详细阐述了Golang反射模型:

interface --> (value, type)
(value, type) --> (value, kind)
(value, type) --> interface
(value, type) --> (value1, type)

最后又加了几道关于进阶话题的菜:

  • 对结构的反射操作
  • 通过反射创建一个匿名函数
  • 通过反射调用函数或方法

希望对读者理解Golang反射模型有一定的帮助。

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

推荐阅读更多精彩内容

  • golang反射机制介绍 Go语言提供了一种机制,在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法...
    发仔很忙阅读 2,234评论 0 0
  • 第一次知道反射的时候还是许多年前在学校里玩 C# 的时候。那时总是弄不清楚这个复杂的玩意能有什么实际用途……然后发...
    勿以浮沙筑高台阅读 1,105评论 0 9
  • 转载请注明出处:Golang 学习笔记(11)—— 反射 介绍 反射是程序执行时检查其所拥有的结构。尤其是类型的一...
    ChainZhang阅读 7,564评论 1 8
  • 首先巴拉巴拉一下golang反射机制的三个定律 1.反射可以从接口类型到反射类型对象 2.反射可以从反射类型对象到...
    吃猫的鱼0阅读 2,812评论 0 1
  • 动效还是欠缺了很多,希望各位能够指正. 个人并不是非常喜欢用框架. 所以想写一些比较实用的功能做一个展示. 只...
    Little_Dragon阅读 1,222评论 9 11