重学Golang系列(一): 深入理解 interface和reflect
前言
interface
(即接口),是Go语言中一个重要的概念和知识点,而功能强大的reflect
正是基于interface
。本文即是对Go语言中的interface
和reflect
基础概念和用法的一次梳理,也算是我阶段学习的总结,以期温故而知新。
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 type
和concrete type
。其中static type
是我们在编码阶段用到的数据类型,如int、string、bool等等;而concrete type
则是runtime
系统看见的类型。
接口类型的变量在类型断言时能否成功,取决于
concrete type
而不是static type
。
在Go语言中指定类型的变量的类型都是静态的,即static type
,其在创建变量的时候就已经确定;而反射主要是配合interface
类型变量来使用的,这些变量的类型都是concrete type
。
在Go的实现中,每个interface
类型的变量都有一个对应的pair
, pair
中记录了实际变量的value
和type
:
(value, type)
interface
类型变量包含了两个指针,分别指向实际变量的值(value)和类型(对应concrete type
)。interface
及其pair
的存在,是Golang实现反射的前提,而反射也正是用来检测接口类型变量内部存储的值和类型的一种机制。说到这里,自然也就要引出reflect
包中的两个数据类Type
和Value
。
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
对象中也包含了实际值的类型信息,通过Value
的Type()
方法将返回具体类型所对应的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.Type
和reflect.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
不是Interface
或Ptr
会panic
;如果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系列(一): 深入理解 interface和reflect的更多相关文章
- 重学c#系列——字典(十一)
前言 重学c#系列继续更新,简单看一下字典的源码. 看源码主要是解释一下江湖中的两个传言: 字典foreach 顺序是字典添加的顺序 字典删除元素后,字典顺序将会改变 正文 那么就从实例化开始看起,这 ...
- 重学c#系列——对c#粗浅的认识(一)
前言 什么是c#呢? 首先你是如何读c#的呢?c sharp?或者c 井? 官方读法是:see sharp. 有没有发现开发多年,然后感觉名字不对. tip:为个人重新整理,如学习还是看官网,c# 文 ...
- 重学c#系列——异常续[异常注意事项](七)
前言 对上节异常的补充,也可以说是异常使用的注意事项. 正文 减少try catch的使用 前面提及到,如果一个方法没有实现该方法的效果,那么就应该抛出异常. 如果有约定那么可以按照约定,如果约定有歧 ...
- 重学数据结构系列之——平衡树之SB Tree(Size Blanced Tree)
学习来源:计蒜客 平衡树 1.定义 对于每一个结点.左右两个子树的高度差的绝对值不超过1,或者叫深度差不超过1 为什么会出现这样一种树呢? 假如我们依照1-n的顺序插入到二叉排序树中,那么二叉排序树就 ...
- 重学c#系列——c# 托管和非托管资源(三)
前言 c# 托管和非托管比较重要,因为这涉及到资源的释放. 现在只要在计算机上运行的,无论玩出什么花来,整个什么概念,逃不过输入数据修改数据输出数据(计算机本质),这里面有个数据的输入,那么我们的内存 ...
- 重学c#系列——c# 托管和非托管资源与代码相关(四)
前言 这是续第三节. 概况垃圾回收与我们写代码的关系: 强引用和弱引用 针对共享 Web 承载优化 垃圾回收和性能 应用程序域资源监视 正文 强引用和弱引用 垃圾回收器不能回收仍在引用的对象的内存-- ...
- 重学c#系列——非托管实例(五)
前言 托管资源到是好,有垃圾回收资源可以帮忙,即使需要我们的一些小小的调试来优化,也是让人感到欣慰的.但是非托管资源就显得苍白无力了,需要程序员自己去设计回收,同样有设计的地方也就能体现出程序员的设计 ...
- 重学c#系列——异常(六)
前言 用户觉得异常是不好的,认为出现异常是写的人的问题. 这是不全面,错误的出现并不总是编写程序的人的原因,有时会因为应用程序的最终用户引发的动作或运行代码的环境而发生错误,比如你用android4去 ...
- 重学c#系列——list(十二)
前言 简单介绍一下list. 正文 这里以list为介绍. private static readonly T[] s_emptyArray = new T[0]; public List() { t ...
随机推荐
- hdu 2844 Coins 多重背包(模板) *
Coins Time Limit: 2000/1 ...
- 题解 yzoj1663: 愤怒的牛(二分) yzoj1662: 曲线(三分)
话说二分和三分的题还没有整理过,就趁这两题来整理下笔记 先讲讲关于二分,对于二分的具体边界长期以来对我来说都是个玄学问题,都是边调边拍改对的.思路大体是确定左边界l,和有边界r,判断满足条件缩小范围. ...
- NOIP2002[提高组] 均分纸牌 题解
题面 题目保证有解即纸牌总数能被人数整除(N|T)每个人持有纸牌a[1]...a[m],我们可以先考虑第一个人 1.若a[1]>T/M,则第一个人需要给第二个人c[1]-T/M张纸牌,即把c[2 ...
- Oracle数据库之七 多表查询
七.多表查询 对于查询在之前已经学过了简单查询.限定查询.查询排序,这些都属于 SQL 的标准语句,而上一章的单行函数,主要功能是为了弥补查询的不足. 而从多表查询开始就正式进入到了复杂查询部 ...
- Vue使用MathJax动态识别数学公式
本人菜鸟一名,如有错误,还请见谅. 1.前言 最近公司的一个项目需求是在前端显示Latex转化的数学公式,经过不断的百度和测试已基本实现.现在此做一个记录. 2.MathJax介绍 MathJax是一 ...
- 【Distributed】缓存技术
一.缓存概述 1.1 缓存技术分类 1.2 缓存框架分类 1.3 Session理解的误区 二.基于Map集合实现本地缓存 2.1 定义Map缓存工具类 2.2 使用案例 三.Ehcache 缓存框架 ...
- Vert.x 之 HelloWorld
Hello World 欢迎来到Vert.x的世界,相信您在接触Vert.x的同时,迫不及待想动手试一试,如您在学习计算机其它知识一样,总是从Hello World开始,下面我们将引导您制作一个最基本 ...
- Windows服务器远程桌面不能复制粘贴的解决方法
今天使用windows 2008服务器,实然就不能从本地复制内容和粘贴内容了,从网上找了下原因,最终解决了.一般本地和服务器不能复制粘贴分两种情况: 情况一:复制粘贴功能原本可以用,突然失灵了. 解决 ...
- [币严区块链]以太坊(ETH)Dapp开发入门教程之宠物商店领养游戏
阅读本文前,你应该对以太坊.智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 除此之外,你最好还了解一些HTML及JavaScript知识. 本文通过实例教大家来开发去中心化应用,应用效果如图 ...
- IBM MQ reason code list
The reason code parameter (Reason) is a qualification to the completion code parameter (CompCode). I ...