重学Golang系列(一): 深入理解 interface和reflect

前言

interface (即接口),是Go语言中一个重要的概念和知识点,而功能强大的 reflect 正是基于 interface 。本文即是对Go语言中的 interfacereflect 基础概念和用法的一次梳理,也算是我阶段学习的总结,以期温故而知新。

interface(接口)

定义

在Go语言中,如果自定义类型(比如 struct )实现了某个 interface 中的所有方法,那么就可以说这个类型实现了这个接口。接口可如下定义:

type 接口名称 interface {
    method1(参数列表) 返回值列表
    method1(参数列表) 返回值列表
    ...
}

interface 是一组方法的集合,但并不需要实现这些方法,并且 interface没有变量interface 中的方法集合可以表示一个对象的特征和能力,当自定义类型需要使用这些方法时,可以根据需要把这些方法实现出来。举个栗子:

package main

import (
    "fmt"
)

type Animal interface {
    Eat()
    Run()
}

type Dog struct {
    Name string
}

type Cat struct {
    Name string
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func (cat *Cat) Eat() {
    fmt.Printf("%s is eating.", cat.Name)
}

func (cat *Cat) Run() {
    fmt.Printf("%s is running.", cat.Name)
}

func main() {
    var animal1 Animal
    animal1 = &Dog{"doggy"}
    animal1.Eat()
    animal1.Run()

    var animal2 Animal
    animal2 = &Cat{"catty"}
    animal2.Eat()
    animal2.Run()
}

上面即定义了一个Animal接口,以及Dog类型和Cat类型。Dog类型和Cat类型都实现了Animal接口中的方法,所以Dog和Cat都是Animal类型。

同时接口 本身不能创建实例 ,但从上例可以看出,接口类型的变量可以指向一个实现了该接口的自定义类型的实例。 interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil

空接口

空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋值给空接口。修改一下上面的main函数:

func main() {
    var animal interface{}
    dog := &Dog{"doggy"}
    animal = dog
    fmt.Println(animal)
}

运行结果:

&{doggy}

接口继承

一个接口可以继承多个其他接口,如果要实现这个接口,那么必须将所继承的所有接口中的方法都实现。

package main

import (
    "fmt"
)

type Eater interface {
    Eat()
}

type Runner interface {
    Run()
}

type Animal interface {
    Eater
    Runner
}

// 这里定义一个Dog的struct,并实现eat方法和run方法,这样就实现了动物的接口
type Dog struct {
    Name string
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func main() {
    var animal1 Animal
    animal1 = &Dog{"doggy"}
    animal1.Eat()
    animal1.Run()
}

类型断言

当我们不确定某个接口变量里存储的是什么类型的变量时,我们可以利用类型断言来判断变量类型。

var animal1 Animal
animal1 = &Dog{"doggy"}
dog := animal1.(*Dog)

在进行类型断言时,如果类型不匹配,就会报 panic , 因此需要加上检测机制,如果成功就 ok,否则也不要报 panic

var animal1 Animal
animal1 = &Dog{"doggy"}

if dog, ok := animal1.(*Dog); ok {
    fmt.Println("convert success")
    dog.Run()
} else {
    fmt.Println("convert fail")
}

另外我们也可以使用 switch-type 语法进行类型断言:

package main

import (
    "fmt"
)

type Eater interface {
    Eat()
}

type Runner interface {
    Run()
}

type Animal interface {
    Eater
    Runner
}

type Dog struct {
    Name string
}

type Cat struct {
    Name string
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func (cat *Cat) Eat() {
    fmt.Printf("%s is eating.", cat.Name)
}

func (cat *Cat) Run() {
    fmt.Printf("%s is running.", cat.Name)
}

func TypeJudge(animals ...interface{}) {
    for index, animal := range animals {
        switch animal.(type) {
        case *Dog:
            fmt.Printf("第%d个参数是Dog类型\n", index)
        case *Cat:
            fmt.Printf("第%d个参数是Cat类型\n", index)
        default:
            fmt.Println("不确定类型")
        }
    }
}

func main() {
    var animal1 Animal
    animal1 = &Dog{"doggy"}

    var animal2 Animal
    animal2 = &Cat{"catty"}

    TypeJudge(animal1, animal2)
}

作用

interface 对于Go语言的意义在于其实现了泛型,比如在一个函数中需要能接收不同类型的参数或者返回不同类型的值,而不是一开始就指定参数或者返回值的类型,这样就可以让函数支持所有类型:

func FuncName(arg1 interface{}, rest ...interface{}) interface{} {
    // ...
}

面向对象语言比如C++、Java都有多态的特性,可以说 interface 是Go语言中实现多态的一种形式。同一个interface,可以让不同的类(自定义类型)实现,从而可以调用同一个函数名的函数但实现完全不同的功能。

有时我们能够利用 interface 实现非常巧妙的功能:通常我们定义一个切片(slice)都会指定一个具体的类型,但是我们有时需要切片中的元素可以任何类型的变量,这个时候 interface 就派上用场了。下面是在go代码中update数据库表中数据时,利用 interface 实现的骚操作,读者可以体会一下 interface 带来的便利:

func generateSQLForUpdatingArticle(article model.ArticleStruct) (string, []interface{}) {
    var columns = make([]string, 0)
    var arguments = make([]interface{}, 0)

    if len(article.CommentCount) > 0 {
        columns = append(columns, "comment_count = ?")
        arguments = append(arguments, article.CommentCount)
    }

    if len(article.Source) > 0 {
        columns = append(columns, "source = ?")
        arguments = append(arguments, article.Source)
    }

    if len(article.Summary) > 0 {
        columns = append(columns, "summary = ?")
        arguments = append(arguments, article.Summary)
    }

    if len(article.Content) > 0 {
        columns = append(columns, "content = ?")
        arguments = append(arguments, article.Content)
    }

    sql := fmt.Sprintf("UPDATE article_structs SET %s WHERE sid = %s", strings.Join(columns, ","), article.Sid)
    return sql, arguments
}

func UpdateArticle(article model.ArticleStruct) error {
    sql, arguments := generateSQLForUpdatingArticle(article)
    if err := db.Exec(sql, arguments...).Error; err != nil {
        log.Println("Updating article failed with error:", err)
        return err
    }
    return nil
}

然而,空接口 interface{} 虽然能保存任意的值,但也带来了一个问题:一个空的接口会隐藏值对应的表示方式和所有的公开的方法,因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值, 对于内部值并没有特别可做的事情;如果我们事先不知道空接口指向的值的具体类型,我们可能就束手无策了。

这个时候我们想要知道一个接口类型的变量具体是什么(什么类型),有什么能力(有哪些方法),就需要一面“镜子”能够反射( reflect )出这个变量的具体内容。在Go语言中也正好有这样的工具—— reflect

reflect(反射)

概念

在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述( self-representation )和监测( examination ),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

在讲反射之前,我们需要了解一下Golang关于类型设计的一些原则:

变量包含两部分:type(类型)和value(值)。

type 分为 static typeconcrete type 。其中 static type 是我们在编码阶段用到的数据类型,如int、string、bool等等;而 concrete type 则是 runtime 系统看见的类型。

接口类型的变量在类型断言时能否成功,取决于 concrete type 而不是 static type

在Go语言中指定类型的变量的类型都是静态的,即 static type ,其在创建变量的时候就已经确定;而反射主要是配合 interface 类型变量来使用的,这些变量的类型都是 concrete type

在Go的实现中,每个 interface 类型的变量都有一个对应的 pair , pair 中记录了实际变量的 valuetype

(value, type)

interface 类型变量包含了两个指针,分别指向实际变量的值(value)和类型(对应 concrete type )。 interface 及其 pair 的存在,是Golang实现反射的前提,而反射也正是用来检测接口类型变量内部存储的值和类型的一种机制。说到这里,自然也就要引出 reflect 包中的两个数据类 TypeValue

reflect.Type和reflect.Value

reflect.Type

reflect 包中 Type 接口定义如下:

type Type interface {
    // Kind返回该接口的具体分类
    Kind() Kind
    // Name返回该类型在自身包内的类型名,如果是未命名类型会返回""
    Name() string
    // PkgPath返回类型的包路径,即明确指定包的import路径,如"encoding/base64"
    // 如果类型为内建类型(string, error)或未命名类型(*T, struct{}, []int),会返回""
    PkgPath() string
    // 返回类型的字符串表示。该字符串可能会使用短包名(如用base64代替"encoding/base64")
    // 也不保证每个类型的字符串表示不同。如果要比较两个类型是否相等,请直接用Type类型比较。
    String() string
    // 返回要保存一个该类型的值需要多少字节;类似unsafe.Sizeof
    Size() uintptr
    // 返回当从内存中申请一个该类型值时,会对齐的字节数
    Align() int
    // 返回当该类型作为结构体的字段时,会对齐的字节数
    FieldAlign() int
    // 如果该类型实现了u代表的接口,会返回真
    Implements(u Type) bool
    // 如果该类型的值可以直接赋值给u代表的类型,返回真
    AssignableTo(u Type) bool
    // 如该类型的值可以转换为u代表的类型,返回真
    ConvertibleTo(u Type) bool
    // 返回该类型的字位数。如果该类型的Kind不是Int、Uint、Float或Complex,会panic
    Bits() int
    // 返回array类型的长度,如非数组类型将panic
    Len() int
    // 返回该类型的元素类型,如果该类型的Kind不是Array、Chan、Map、Ptr或Slice,会panic
    Elem() Type
    // 返回map类型的键的类型。如非映射类型将panic
    Key() Type
    // 返回一个channel类型的方向,如非通道类型将会panic
    ChanDir() ChanDir

    // 返回struct类型的字段数(匿名字段算作一个字段),如非结构体类型将panic
    NumField() int
    // 返回struct类型的第i个字段的类型,如非结构体或者i不在[0, NumField())内将会panic
    Field(i int) StructField
    // 返回索引序列指定的嵌套字段的类型,
    // 等价于用索引中每个值链式调用本方法,如非结构体将会panic
    FieldByIndex(index []int) StructField
    // 返回该类型名为name的字段(会查找匿名字段及其子字段),
    // 布尔值说明是否找到,如非结构体将panic
    FieldByName(name string) (StructField, bool)
    // 返回该类型第一个字段名满足函数match的字段,布尔值说明是否找到,如非结构体将会panic
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    // 如果函数类型的最后一个输入参数是"..."形式的参数,IsVariadic返回真
    // 如果这样,t.In(t.NumIn() - 1)返回参数的隐式的实际类型(声明类型的切片)
    // 如非函数类型将panic
    IsVariadic() bool
    // 返回func类型的参数个数,如果不是函数,将会panic
    NumIn() int
    // 返回func类型的第i个参数的类型,如非函数或者i不在[0, NumIn())内将会panic
    In(i int) Type
    // 返回func类型的返回值个数,如果不是函数,将会panic
    NumOut() int
    // 返回func类型的第i个返回值的类型,如非函数或者i不在[0, NumOut())内将会panic
    Out(i int) Type
    // 返回该类型的方法集中方法的数目
    // 匿名字段的方法会被计算;主体类型的方法会屏蔽匿名字段的同名方法;
    // 匿名字段导致的歧义方法会滤除
    NumMethod() int
    // 返回该类型方法集中的第i个方法,i不在[0, NumMethod())范围内时,将导致panic
    // 对非接口类型T或*T,返回值的Type字段和Func字段描述方法的未绑定函数状态
    // 对接口类型,返回值的Type字段描述方法的签名,Func字段为nil
    Method(int) Method
    // 根据方法名返回该类型方法集中的方法,使用一个布尔值说明是否发现该方法
    // 对非接口类型T或*T,返回值的Type字段和Func字段描述方法的未绑定函数状态
    // 对接口类型,返回值的Type字段描述方法的签名,Func字段为nil
    MethodByName(string) (Method, bool)
    // 内含隐藏或非导出方法
}

我们可以通过 reflect.TypeOf 接受任意 interface{} 类型,并返回对应的动态类型 reflect.Type

num := reflect.TypeOf(1)
fmt.Println(num.String())
fmt.Println(num)

看一下 TypeOf() 的实现代码:

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

可以发现 TypeOf 函数的参数类型是一个 interface{} ,并且在函数内部将这里的具体值1进行一个隐式转换,转换为一个空接口类型的变量,这个变量包含两部分信息:1这个变量的动态类型(为int)和动态值(为1);最后 TypeOf 的返回值是 reflect.Type 类型(我们称为 反射类型对象 ),这样就能够调用上面Type接口的方法获取所需的变量信息。

  • 当反射对象的类型是原始数据类型时:
func main() {
    var s string
    rString := reflect.TypeOf(s)
    fmt.Println(rString)         //string
    fmt.Println(rString.Name())  //string,返回表示类型名称的字符串
    fmt.Println(rString.Kind())  //string,返回 reflect.Kind 类型的常量
}
  • 当反射对象的类型是指针类型时:
type Dog struct {
    Name string
    Age  int
}

func main() {
    dogPtr := &Dog{"doggy"}
    rDogPtr := reflect.TypeOf(dogPtr)
    
    fmt.Println(rDogPtr.Name())  // 为空
    fmt.Println(rDogPtr.Kind())  // ptr
    
    // Elem()可以获取指针指向的实际变量
    rDog := rDogPtr.Elem()
    fmt.Println(rDogPtr.Name())  // Dog
    fmt.Println(rDogPtr.Kind())  // struct
}

可以发现从指针获取反射对象时,不能直接使用 Name()Kind() ,这样只能得到该指针的信息。这时可以使用 Elem() 获取指针指向的实际变量。

  • 当反射对象的类型是结构体类型时:

如果反射对象的类型是结构体,可以通过 NumField()Field() 方法获得结构体成员的详细信息。

type Dog struct {
    Name string
    Age  int
}

func main() {
    dog := Dog{"doggy", 2}
    rDog := reflect.TypeOf(dog)

    fmt.Printf("%v ", rDog.Name()) // Dog
    fmt.Println(rDog.Kind())       // struct

    for index := 0; index < rDog.NumField(); index++ {
        fmt.Printf("%v ", rDog.Field(index).Name)
        fmt.Println(rDog.Field(index).Type)
    }
}

运行输出:

Dog struct
Name string
Age int

reflect.Value

reflect包中 Value 类型定义如下:

type Value struct {
    // typ holds the type of the value represented by a Value.
    typ *rtype

    // Pointer-valued data or, if flagIndir is set, pointer to data.
    // Valid when either flagIndir is set or typ.pointers() is true.
    ptr unsafe.Pointer
    
    // flag holds metadata about the value.
    flag
}

可以看到 Value 类型包含一个类型指针、一个值指针以及标志信息。同时 Value 类型还有很多方法,其中用于获取值方法:

func (v Value) Int() int64 // 获取int类型值,如果 v 值不是有符号整型,则 panic。

func (v Value) Uint() uint64 // 获取unit类型的值,如果 v 值不是无符号整型(包括 uintptr),则 panic。

func (v Value) Float() float64 // 获取float类型的值,如果 v 值不是浮点型,则 panic。

func (v Value) Complex() complex128 // 获取复数类型的值,如果 v 值不是复数型,则 panic。

func (v Value) Bool() bool // 获取布尔类型的值,如果 v 值不是布尔型,则 panic。

func (v Value) Len() int // 获取 v 值的长度,v 值必须是字符串、数组、切片、映射、通道。

func (v Value) Cap() int  // 获取 v 值的容量,v 值必须是数值、切片、通道。

func (v Value) Index(i int) reflect.Value // 获取 v 值的第 i 个元素,v 值必须是字符串、数组、切片,i 不能超出范围。

func (v Value) Bytes() []byte // 获取字节类型的值,如果 v 值不是字节切片,则 panic。

func (v Value) Slice(i, j int) reflect.Value // 获取 v 值的切片,切片长度 = j - i,切片容量 = v.Cap() - i。
// v 必须是字符串、数值、切片,如果是数组则必须可寻址。i 不能超出范围。

func (v Value) Slice3(i, j, k int) reflect.Value  // 获取 v 值的切片,切片长度 = j - i,切片容量 = k - i。
// i、j、k 不能超出 v 的容量。i <= j <= k。
// v 必须是字符串、数值、切片,如果是数组则必须可寻址。i 不能超出范围。

func (v Value) MapIndex(key Value) reflect.Value // 根据 key 键获取 v 值的内容,v 值必须是映射。
// 如果指定的元素不存在,或 v 值是未初始化的映射,则返回零值(reflect.ValueOf(nil))

func (v Value) MapKeys() []reflect.Value // 获取 v 值的所有键的无序列表,v 值必须是映射。
// 如果 v 值是未初始化的映射,则返回空列表。

func (v Value) OverflowInt(x int64) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是有符号整型。

func (v Value) OverflowUint(x uint64) bool  // 判断 x 是否超出 v 值的取值范围,v 值必须是无符号整型。

func (v Value) OverflowFloat(x float64) bool  // 判断 x 是否超出 v 值的取值范围,v 值必须是浮点型。

func (v Value) OverflowComplex(x complex128) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是复数型。

用于设置值方法:

func (v Value) SetUint(x uint64)  // 设置无符号整型的值

func (v Value) SetFloat(x float64) // 设置浮点类型的值

func (v Value) SetComplex(x complex128) //设置复数类型的值

func (v Value) SetBool(x bool) //设置布尔类型的值

func (v Value) SetString(x string) //设置字符串类型的值

func (v Value) SetLen(n int)  // 设置切片的长度,n 不能超出范围,不能为负数。

func (v Value) SetCap(n int) //设置切片的容量

func (v Value) SetBytes(x []byte) //设置字节类型的值

func (v Value) SetMapIndex(key, val reflect.Value) //设置map的key和value,前提必须是初始化以后,存在覆盖、不存在添加

func (v Value) Set(x Value) // 将v的持有值修改为x的持有值。如果v.CanSet()返回假,会panic。x的持有值必须能直接赋给v持有值的类型。

其他方法:

结构体相关:
func (v Value) NumField() int // 获取结构体字段(成员)数量

func (v Value) Field(i int) reflect.Value  //根据索引获取结构体字段

func (v Value) FieldByIndex(index []int) reflect.Value // 根据索引链获取结构体嵌套字段

func (v Value) FieldByName(string) reflect.Value // 根据名称获取结构体的字段,不存在返回reflect.ValueOf(nil)

func (v Value) FieldByNameFunc(match func(string) bool) Value // 根据匹配函数 match 获取字段,如果没有匹配的字段,则返回零值(reflect.ValueOf(nil))


通道相关:
func (v Value) Send(x reflect.Value)// 发送数据(会阻塞),v 值必须是可写通道。

func (v Value) Recv() (x reflect.Value, ok bool) // 接收数据(会阻塞),v 值必须是可读通道。

func (v Value) TrySend(x reflect.Value) bool // 尝试发送数据(不会阻塞),v 值必须是可写通道。

func (v Value) TryRecv() (x reflect.Value, ok bool) // 尝试接收数据(不会阻塞),v 值必须是可读通道。

func (v Value) Close() // 关闭通道


函数相关
func (v Value) Call(in []Value) (r []Value) // 通过参数列表 in 调用 v 值所代表的函数(或方法)。函数的返回值存入 r 中返回。
// 要传入多少参数就在 in 中存入多少元素。
// Call 即可以调用定参函数(参数数量固定),也可以调用变参函数(参数数量可变)。

func (v Value) CallSlice(in []Value) []Value // 调用变参函数

同样地,我们可以通过 reflect.ValueOf 接受任意interface{}类型,并返回对应的动态类型 reflect.Value

v := reflect.ValueOf(2)
fmt.Println(v)  // 2
fmt.Println(v.String()) // <int Value>

看一下 reflect.ValueOf 的实现代码:

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

    // TODO: Maybe allow contents of a Value to live on the stack.
    // For now we make the contents always escape to the heap. It
    // makes life easier in a few places (see chanrecv/mapassign
    // comment below).
    escapes(i)

    return unpackEface(i)
}

// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
    e := (*emptyInterface)(unsafe.Pointer(&i))
    // NOTE: don't read e.word until we know whether it is really a pointer or not.
    t := e.typ
    if t == nil {
        return Value{}
    }
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f}
}

escapes() 涉及栈和堆的对象分配以及逃逸分析,有兴趣的可以看 William Kennedy 写的系列文章: Go 语言机制之逃逸分析

reflect.TypeOf 类似, ValueOf 函数的参数类型是一个interface{},在函数内部将入参进行一个隐式转换,转换为一个空接口类型的变量,最终返回一个 Value 对象,并且 reflect.ValueOf 返回值也是 反射类型对象

可以注意到 Value 对象中也包含了实际值的类型信息,通过 ValueType() 方法将返回具体类型所对应的 reflect.Type :

v := reflect.ValueOf(2)
t := v.Type()
fmt.Println(t) // int
fmt.Println(t.String()) // int

通过 relfect.Value 获取实际变量的信息

现在我们知道了通过 reflect.ValueOf 可以将 接口类型变量 转换成 反射类型变量 ,当然我们也可以通过 reflect.Value.Interface 方法逆操作回去,然后通过断言的方式得到实际值:

v := reflect.ValueOf(2)
i := v.Interface()
if num, ok := i.(int); ok { // 类型断言
    fmt.Println(num)
}

但通常在实际场景中,我们其实并不知道原始值的类型,这里就需要利用 reflect.Typereflect.Value 的方法探索原始值的信息。下面通过一个例子说明:

package main

import (
    "fmt"
    "reflect"
)

type Dog struct {
    Name string
    Age  int
}

func (dog *Dog) Eat() {
    fmt.Printf("%s is eating.", dog.Name)
}

func (dog *Dog) Run() {
    fmt.Printf("%s is running.", dog.Name)
}

func (dog Dog) Sleep() {
    fmt.Printf("%s is sleeping.", dog.Name)
}

func (dog Dog) Jump() {
    fmt.Printf("%s is jumping.", dog.Name)
}

func main() {
    doggy := Dog{"doggy", 2}
    checkFieldAndMethod(doggy)

    fmt.Println("")
    tommy := &Dog{"tommy", 2}
    checkFieldAndMethod(tommy)
}

func checkFieldAndMethod(input interface{}) {
    inputType := reflect.TypeOf(input)
    fmt.Println("Type of input is :", inputType.Name())
    inputValue := reflect.ValueOf(input)
    fmt.Println("Value of input is :", inputValue)

    // 如果input原始类型时指针,通过Elem()方法或者Indirect()获取指针指向的值
    if inputValue.Kind() == reflect.Ptr {
        inputValue = inputValue.Elem()
        // inputValue = reflect.Indirect(inputValue)
        fmt.Println("Value input points to is :", inputValue)
    }

    //使用NumField()得到结构体中字段的数量,遍历得到字段的值Field(i)和类型Field(i).Type()
    for i := 0; i < inputValue.NumField(); i++ {
        field := inputValue.Type().Field(i)
        value := inputValue.Field(i).Interface()
        fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
    }

    // 获取方法
    for i := 0; i < inputType.NumMethod(); i++ {
        m := inputType.Method(i)
        fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
}

运行之后输出:

Type of input is : Dog
Value of input is : {doggy 2}
Name: string = doggy
Age: int = 2
Jump: func(main.Dog)
Sleep: func(main.Dog)

Type of input is : 
Value of input is : &{tommy 2}
Value input points to is : {tommy 2}
Name: string = tommy
Age: int = 2
Eat: func(*main.Dog)
Jump: func(*main.Dog)
Run: func(*main.Dog)
Sleep: func(*main.Dog)

利用反射获取原始值得类型和方法的步骤如下:

  • 判断原始值是值变量还是指针变量,如果是指针变量,则通过 Elem() 方法或者 Indirect() 获取指针指向的值;
  • 使用 NumField() 得到结构体中字段的数量,遍历得到字段的值 Field(i) 和类型 Field(i).Type()
  • 使用 NumMethod() 得到结构体的方法,遍历得到方法的名称和类型。

另外,在使用 reflect.Value 过程有时会对 Elem() 方法和 Indirect() 有些迷惑,搞不清这两个方法的区别,这里总结一下:

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value

// Indirect returns the value that v points to.
// If v is a nil pointer, Indirect returns a zero Value.
// If v is not a pointer, Indirect returns v.
func Indirect(v Value) Value
  • Elem 返回v持有的接口保管的值的 Value 封装,或者v持有的指针指向的值的 Value 封装。如果v的 Kind 不是 InterfacePtrpanic ;如果v持有的值为 nil ,会返回 Value 零值。
  • Indirect 返回v持有的指针指向的值的 Value 封装。如果v持有的值为 nil ,会返回 Value 零值。如果v持有的变量不是指针,那么将返回原值v。

也就是说,当v持有的变量是指针时, Elem() 方法和 Indirect() 是等价的。

细心的读者可能发现对于值变量和指针变量,通过反射获取到的变量方法有些差异,这个问题就留给读者自己思考吧。

通过 relfect.Value 修改实际变量的信息

当通过 relfect.Value 修改实际变量的信息是常用到以下反射值对象的方法:

func (v Value) Elem() Value  
//Elem()返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装,类似于*操作,此时的Value表示的是Value的元素且可以寻址。

func (v Value) Addr() Value 
//Addr()返回一个持有指向v变量地址的指针的Value封装,类似于&操作。

func (v Value) CanAddr() bool
//CanAddr()返回是否可以获取v持有值的指针。可以获取指针的值被称为可寻址的。

func (v Value) CanSet() bool
//CanSet()返回v持有的值是否可以被修改

然而,值得注意的是并不是所有 reflect.Value 类型的反射值都可以修改,考虑下面这个例子:

package main 

import(
    "fmt"
    "reflect"
)

func main() {
    a := 1
    rA := reflect.ValueOf(a)
    fmt.Println(rA.CanSet()) //false

    rAptr := reflect.ValueOf(&a)
    rA2 := rAptr.Elem()
    fmt.Println(rA2.CanSet()) //true
    rA2.SetInt(2)
    fmt.Println(rA2.Int()) //2
}

修改反射类型变量的值有两个条件:

addressable

有一些修改反射类型变量是可寻址的,有一些则不是:

package main

import (
    "reflect"
    "fmt"
)

func main() {
    x := 2
    a := reflect.ValueOf(2)
    b := reflect.ValueOf(x)
    c := reflect.ValueOf(&x)
    d := c.Elem()
    fmt.Println(a.CanAddr()) // false
    fmt.Println(b.CanAddr()) // false
    fmt.Println(c.CanAddr()) // false
    fmt.Println(d.CanAddr()) // true

}

对于非指针变量x,通过 reflect.ValueOf(x) 返回的 reflect.Value 是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可 取地址的。我们可以通过调用 reflect.ValueOf(&x).Elem() ,获取到x对应的可取地址的反射值。

对于结构体类型变量,如果成员字段没有导出,那么虽然可以被访问,但不能通过反射修改:

package main

import (
    "fmt"
    "reflect"
)

type Dog struct {
    Name string
    Age  int
    sex  string
}

func main() {
    rDog := reflect.ValueOf(&Dog{}).Elem()
    vAge := rDog.FieldByName("Age")
    vAge.SetInt(1)

    vSex := rDog.FieldByName("sex")
    vSex.SetString("male")
}

运行出现报错:SetString使用的值来自于一个未导出的字段。

panic: reflect: reflect.Value.SetString using value obtained using unexported field

为了能修改这个值,需要将该字段导出。将Dog类型中的 sex成员首字母大写即可。

修改可取地址的 reflect.Value 持有的变量值,除了可以通过反射的Set系列方法,还可以通过从反射类型变量获取实际值的指针来修改:

package main

import (
    "reflect"
    "fmt"
)

func main() {
    x := 1
    v := reflect.ValueOf(&x).Elem()
    px := v.Addr().Interface().(*int)
    *px = 2
    fmt.Print(x) //2
}

首先调用 Addr() 方法,返回 一个持有指向变量的指针的 Value ;然后在 Value 上调用 Interface() 方法,返回一个 interface{} ,里面包含指向变量的指针;最后通过类型断言得到普通指针来修改变量的值。

通过反射调用函数

如果反射值对象( reflect.Value )持有值的类型为函数时,可以通过 reflect.Value 调用该函数。

func (v Value) Call(in []Value) []Value

Call 方法使用输入的参数in调用v持有的函数。参数in是反射值对象的切片,即 []reflect.Value ;调用完成时,函数的返回值通过 []reflect.Value 返回。

package main 

import(
    "fmt"
    "reflect"
)
func add(a, b int) int {

    return a + b
}

func main() {

    // 将函数add包装为反射值对象
    funcValue := reflect.ValueOf(add)

    // 构造函数add的参数, 传入两个整型值
    paramList := []reflect.Value{reflect.ValueOf(5), reflect.ValueOf(10)}

    // 反射调用函数Call()
    retList := funcValue.Call(paramList)

    // 获取第一个返回值, 取整数值
    fmt.Println(retList[0].Int()) //返回 15
}

如果需要通过反射调用结构体的方法,可以利用 MethodByName 方法来完成:

func (v Value) MethodByName(name string) Value
//返回v的名为name的方法的已绑定(到v的持有值的)状态的函数形式的Value封装。

举例:

package main 

import(
    "fmt"
    "reflect"
)

type Dog struct {
    Name string
    Age  int
}

func (dog *Dog) SetName(name string){
    dog.Name = name
}

func main() {
    dog := Dog{}
    rDog := reflect.ValueOf(&dog)
    paramList1 := []reflect.Value{reflect.ValueOf("doggy")}
    rDog.MethodByName("SetName").Call(paramList1)
    fmt.Println(dog.Name) //doggy
}

值得注意的是,反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value ,用户还需要从中取出调用值。因此反射调用函数的 性能问题尤为突出 ,不建议大量使用反射函数调用。

总结

本文介绍了Go语言中 interface 的定义、用法以及副作用,并由此引入 reflect ,通过大量示例详细介绍了 reflect 的概念,通过 reflect 获取值、修改值的用法,以及调用函数的用法。内容上可以说相当详实具体了,在此过程中也让笔者自己对这部分的知识有了更深刻的认识,也希望有幸能带给读者一点帮助吧。

参考资料

【Golang标准库文档】

【Golang的反射reflect深入理解和示例】

【Go addressable 详解】

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章