深入理解Golang之interface和reflect
本文内容纲要:
-前言
-interface(接口)
-reflect(反射)
-总结
-参考资料
前言
interface
(即接口),是Go语言中一个重要的概念和知识点,而功能强大的reflect
正是基于interface
。本文即是对Go语言中的interface
和reflect
基础概念和用法的一次梳理,也算是我阶段学习的总结,以期温故而知新。
interface(接口)
定义
在Go语言中,如果自定义类型(比如struct
)实现了某个interface
中的所有方法,那么就可以说这个类型实现了这个接口。接口可如下定义:
type接口名称interface{
method1(参数列表)返回值列表
method1(参数列表)返回值列表
...
}
interface
是一组方法的集合,但并不需要实现这些方法,并且interface
中没有变量。interface
中的方法集合可以表示一个对象的特征和能力,当自定义类型需要使用这些方法时,可以根据需要把这些方法实现出来。举个栗子:
packagemain
import(
"fmt"
)
typeAnimalinterface{
Eat()
Run()
}
typeDogstruct{
Namestring
}
typeCatstruct{
Namestring
}
func(dog*Dog)Eat(){
fmt.Printf("%siseating.",dog.Name)
}
func(dog*Dog)Run(){
fmt.Printf("%sisrunning.",dog.Name)
}
func(cat*Cat)Eat(){
fmt.Printf("%siseating.",cat.Name)
}
func(cat*Cat)Run(){
fmt.Printf("%sisrunning.",cat.Name)
}
funcmain(){
varanimal1Animal
animal1=&Dog{"doggy"}
animal1.Eat()
animal1.Run()
varanimal2Animal
animal2=&Cat{"catty"}
animal2.Eat()
animal2.Run()
}
上面即定义了一个Animal接口,以及Dog类型和Cat类型。Dog类型和Cat类型都实现了Animal接口中的方法,所以Dog和Cat都是Animal类型。
同时接口本身不能创建实例,但从上例可以看出,接口类型的变量可以指向一个实现了该接口的自定义类型的实例。interface
类型默认是一个指针(引用类型),如果没有对interface
初始化就使用,那么会输出nil
。
空接口
空接口interface{}
没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋值给空接口。修改一下上面的main函数:
funcmain(){
varanimalinterface{}
dog:=&Dog{"doggy"}
animal=dog
fmt.Println(animal)
}
运行结果:
&{doggy}
接口继承
一个接口可以继承多个其他接口,如果要实现这个接口,那么必须将所继承的所有接口中的方法都实现。
packagemain
import(
"fmt"
)
typeEaterinterface{
Eat()
}
typeRunnerinterface{
Run()
}
typeAnimalinterface{
Eater
Runner
}
//这里定义一个Dog的struct,并实现eat方法和run方法,这样就实现了动物的接口
typeDogstruct{
Namestring
}
func(dog*Dog)Eat(){
fmt.Printf("%siseating.",dog.Name)
}
func(dog*Dog)Run(){
fmt.Printf("%sisrunning.",dog.Name)
}
funcmain(){
varanimal1Animal
animal1=&Dog{"doggy"}
animal1.Eat()
animal1.Run()
}
类型断言
当我们不确定某个接口变量里存储的是什么类型的变量时,我们可以利用类型断言来判断变量类型。
varanimal1Animal
animal1=&Dog{"doggy"}
dog:=animal1.(*Dog)
在进行类型断言时,如果类型不匹配,就会报panic
,因此需要加上检测机制,如果成功就ok,否则也不要报panic
。
varanimal1Animal
animal1=&Dog{"doggy"}
ifdog,ok:=animal1.(*Dog);ok{
fmt.Println("convertsuccess")
dog.Run()
}else{
fmt.Println("convertfail")
}
另外我们也可以使用switch-type
语法进行类型断言:
packagemain
import(
"fmt"
)
typeEaterinterface{
Eat()
}
typeRunnerinterface{
Run()
}
typeAnimalinterface{
Eater
Runner
}
typeDogstruct{
Namestring
}
typeCatstruct{
Namestring
}
func(dog*Dog)Eat(){
fmt.Printf("%siseating.",dog.Name)
}
func(dog*Dog)Run(){
fmt.Printf("%sisrunning.",dog.Name)
}
func(cat*Cat)Eat(){
fmt.Printf("%siseating.",cat.Name)
}
func(cat*Cat)Run(){
fmt.Printf("%sisrunning.",cat.Name)
}
funcTypeJudge(animals...interface{}){
forindex,animal:=rangeanimals{
switchanimal.(type){
case*Dog:
fmt.Printf("第%d个参数是Dog类型\n",index)
case*Cat:
fmt.Printf("第%d个参数是Cat类型\n",index)
default:
fmt.Println("不确定类型")
}
}
}
funcmain(){
varanimal1Animal
animal1=&Dog{"doggy"}
varanimal2Animal
animal2=&Cat{"catty"}
TypeJudge(animal1,animal2)
}
作用
interface
对于Go语言的意义在于其实现了泛型,比如在一个函数中需要能接收不同类型的参数或者返回不同类型的值,而不是一开始就指定参数或者返回值的类型,这样就可以让函数支持所有类型:
funcFuncName(arg1interface{},rest...interface{})interface{}{
//...
}
面向对象语言比如C++、Java都有多态的特性,可以说interface
是Go语言中实现多态的一种形式。同一个interface,可以让不同的类(自定义类型)实现,从而可以调用同一个函数名的函数但实现完全不同的功能。
有时我们能够利用interface
实现非常巧妙的功能:通常我们定义一个切片(slice)都会指定一个具体的类型,但是我们有时需要切片中的元素可以任何类型的变量,这个时候interface
就派上用场了。下面是在go代码中update数据库表中数据时,利用interface
实现的骚操作,读者可以体会一下interface
带来的便利:
funcgenerateSQLForUpdatingArticle(articlemodel.ArticleStruct)(string,[]interface{}){
varcolumns=make([]string,0)
vararguments=make([]interface{},0)
iflen(article.CommentCount)>0{
columns=append(columns,"comment_count=?")
arguments=append(arguments,article.CommentCount)
}
iflen(article.Source)>0{
columns=append(columns,"source=?")
arguments=append(arguments,article.Source)
}
iflen(article.Summary)>0{
columns=append(columns,"summary=?")
arguments=append(arguments,article.Summary)
}
iflen(article.Content)>0{
columns=append(columns,"content=?")
arguments=append(arguments,article.Content)
}
sql:=fmt.Sprintf("UPDATEarticle_structsSET%sWHEREsid=%s",strings.Join(columns,","),article.Sid)
returnsql,arguments
}
funcUpdateArticle(articlemodel.ArticleStruct)error{
sql,arguments:=generateSQLForUpdatingArticle(article)
iferr:=db.Exec(sql,arguments...).Error;err!=nil{
log.Println("Updatingarticlefailedwitherror:",err)
returnerr
}
returnnil
}
然而,空接口interface{}
虽然能保存任意的值,但也带来了一个问题:一个空的接口会隐藏值对应的表示方式和所有的公开的方法,因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值,对于内部值并没有特别可做的事情;如果我们事先不知道空接口指向的值的具体类型,我们可能就束手无策了。
这个时候我们想要知道一个接口类型的变量具体是什么(什么类型),有什么能力(有哪些方法),就需要一面“镜子”能够反射(reflect
)出这个变量的具体内容。在Go语言中也正好有这样的工具——reflect
。
reflect(反射)
概念
在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(
self-representation
)和监测(examination
),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
在讲反射之前,我们需要了解一下Golang关于类型设计的一些原则:
变量包含两部分:type(类型)和value(值)。
type分为
statictype
和concretetype
。其中statictype
是我们在编码阶段用到的数据类型,如int、string、bool等等;而concretetype
则是runtime
系统看见的类型。
接口类型的变量在类型断言时能否成功,取决于
concretetype
而不是statictype
。
在Go语言中指定类型的变量的类型都是静态的,即statictype
,其在创建变量的时候就已经确定;而反射主要是配合interface
类型变量来使用的,这些变量的类型都是concretetype
。
在Go的实现中,每个interface
类型的变量都有一个对应的pair
,pair
中记录了实际变量的value
和type
:
(value,type)
interface
类型变量包含了两个指针,分别指向实际变量的值(value)和类型(对应concretetype
)。interface
及其pair
的存在,是Golang实现反射的前提,而反射也正是用来检测接口类型变量内部存储的值和类型的一种机制。说到这里,自然也就要引出reflect
包中的两个数据类Type
和Value
。
reflect.Type和reflect.Value
reflect.Type
reflect
包中Type
接口定义如下:
typeTypeinterface{
//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(uType)bool
//如果该类型的值可以直接赋值给u代表的类型,返回真
AssignableTo(uType)bool
//如该类型的值可以转换为u代表的类型,返回真
ConvertibleTo(uType)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(iint)StructField
//返回索引序列指定的嵌套字段的类型,
//等价于用索引中每个值链式调用本方法,如非结构体将会panic
FieldByIndex(index[]int)StructField
//返回该类型名为name的字段(会查找匿名字段及其子字段),
//布尔值说明是否找到,如非结构体将panic
FieldByName(namestring)(StructField,bool)
//返回该类型第一个字段名满足函数match的字段,布尔值说明是否找到,如非结构体将会panic
FieldByNameFunc(matchfunc(string)bool)(StructField,bool)
//如果函数类型的最后一个输入参数是"..."形式的参数,IsVariadic返回真
//如果这样,t.In(t.NumIn()-1)返回参数的隐式的实际类型(声明类型的切片)
//如非函数类型将panic
IsVariadic()bool
//返回func类型的参数个数,如果不是函数,将会panic
NumIn()int
//返回func类型的第i个参数的类型,如非函数或者i不在[0,NumIn())内将会panic
In(iint)Type
//返回func类型的返回值个数,如果不是函数,将会panic
NumOut()int
//返回func类型的第i个返回值的类型,如非函数或者i不在[0,NumOut())内将会panic
Out(iint)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()
的实现代码:
//TypeOfreturnsthereflectionTypethatrepresentsthedynamictypeofi.
//Ifiisanilinterfacevalue,TypeOfreturnsnil.
funcTypeOf(iinterface{})Type{
eface:=*(*emptyInterface)(unsafe.Pointer(&i))
returntoType(eface.typ)
}
可以发现TypeOf
函数的参数类型是一个interface{}
,并且在函数内部将这里的具体值1进行一个隐式转换,转换为一个空接口类型的变量,这个变量包含两部分信息:1这个变量的动态类型(为int)和动态值(为1);最后TypeOf
的返回值是reflect.Type
类型(我们称为反射类型对象),这样就能够调用上面Type接口的方法获取所需的变量信息。
-
当反射对象的类型是原始数据类型时:
funcmain(){ varsstring rString:=reflect.TypeOf(s) fmt.Println(rString)//string fmt.Println(rString.Name())//string,返回表示类型名称的字符串 fmt.Println(rString.Kind())//string,返回reflect.Kind类型的常量 }
-
当反射对象的类型是指针类型时:
typeDogstruct{ Namestring Ageint }
funcmain(){ 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()
方法获得结构体成员的详细信息。
typeDogstruct{
Namestring
Ageint
}
funcmain(){
dog:=Dog{"doggy",2}
rDog:=reflect.TypeOf(dog)
fmt.Printf("%v",rDog.Name())//Dog
fmt.Println(rDog.Kind())//struct
forindex:=0;index<rDog.NumField();index++{
fmt.Printf("%v",rDog.Field(index).Name)
fmt.Println(rDog.Field(index).Type)
}
}
运行输出:
Dogstruct
Namestring
Ageint
reflect.Value
reflect包中Value
类型定义如下:
typeValuestruct{
//typholdsthetypeofthevaluerepresentedbyaValue.
typ*rtype
//Pointer-valueddataor,ifflagIndirisset,pointertodata.
//ValidwheneitherflagIndirissetortyp.pointers()istrue.
ptrunsafe.Pointer
//flagholdsmetadataaboutthevalue.
flag
}
可以看到Value
类型包含一个类型指针、一个值指针以及标志信息。同时Value
类型还有很多方法,其中用于获取值方法:
func(vValue)Int()int64//获取int类型值,如果v值不是有符号整型,则panic。
func(vValue)Uint()uint64//获取unit类型的值,如果v值不是无符号整型(包括uintptr),则panic。
func(vValue)Float()float64//获取float类型的值,如果v值不是浮点型,则panic。
func(vValue)Complex()complex128//获取复数类型的值,如果v值不是复数型,则panic。
func(vValue)Bool()bool//获取布尔类型的值,如果v值不是布尔型,则panic。
func(vValue)Len()int//获取v值的长度,v值必须是字符串、数组、切片、映射、通道。
func(vValue)Cap()int//获取v值的容量,v值必须是数值、切片、通道。
func(vValue)Index(iint)reflect.Value//获取v值的第i个元素,v值必须是字符串、数组、切片,i不能超出范围。
func(vValue)Bytes()[]byte//获取字节类型的值,如果v值不是字节切片,则panic。
func(vValue)Slice(i,jint)reflect.Value//获取v值的切片,切片长度=j-i,切片容量=v.Cap()-i。
//v必须是字符串、数值、切片,如果是数组则必须可寻址。i不能超出范围。
func(vValue)Slice3(i,j,kint)reflect.Value//获取v值的切片,切片长度=j-i,切片容量=k-i。
//i、j、k不能超出v的容量。i<=j<=k。
//v必须是字符串、数值、切片,如果是数组则必须可寻址。i不能超出范围。
func(vValue)MapIndex(keyValue)reflect.Value//根据key键获取v值的内容,v值必须是映射。
//如果指定的元素不存在,或v值是未初始化的映射,则返回零值(reflect.ValueOf(nil))
func(vValue)MapKeys()[]reflect.Value//获取v值的所有键的无序列表,v值必须是映射。
//如果v值是未初始化的映射,则返回空列表。
func(vValue)OverflowInt(xint64)bool//判断x是否超出v值的取值范围,v值必须是有符号整型。
func(vValue)OverflowUint(xuint64)bool//判断x是否超出v值的取值范围,v值必须是无符号整型。
func(vValue)OverflowFloat(xfloat64)bool//判断x是否超出v值的取值范围,v值必须是浮点型。
func(vValue)OverflowComplex(xcomplex128)bool//判断x是否超出v值的取值范围,v值必须是复数型。
用于设置值方法:
func(vValue)SetUint(xuint64)//设置无符号整型的值
func(vValue)SetFloat(xfloat64)//设置浮点类型的值
func(vValue)SetComplex(xcomplex128)//设置复数类型的值
func(vValue)SetBool(xbool)//设置布尔类型的值
func(vValue)SetString(xstring)//设置字符串类型的值
func(vValue)SetLen(nint)//设置切片的长度,n不能超出范围,不能为负数。
func(vValue)SetCap(nint)//设置切片的容量
func(vValue)SetBytes(x[]byte)//设置字节类型的值
func(vValue)SetMapIndex(key,valreflect.Value)//设置map的key和value,前提必须是初始化以后,存在覆盖、不存在添加
func(vValue)Set(xValue)//将v的持有值修改为x的持有值。如果v.CanSet()返回假,会panic。x的持有值必须能直接赋给v持有值的类型。
其他方法:
结构体相关:
func(vValue)NumField()int//获取结构体字段(成员)数量
func(vValue)Field(iint)reflect.Value//根据索引获取结构体字段
func(vValue)FieldByIndex(index[]int)reflect.Value//根据索引链获取结构体嵌套字段
func(vValue)FieldByName(string)reflect.Value//根据名称获取结构体的字段,不存在返回reflect.ValueOf(nil)
func(vValue)FieldByNameFunc(matchfunc(string)bool)Value//根据匹配函数match获取字段,如果没有匹配的字段,则返回零值(reflect.ValueOf(nil))
通道相关:
func(vValue)Send(xreflect.Value)//发送数据(会阻塞),v值必须是可写通道。
func(vValue)Recv()(xreflect.Value,okbool)//接收数据(会阻塞),v值必须是可读通道。
func(vValue)TrySend(xreflect.Value)bool//尝试发送数据(不会阻塞),v值必须是可写通道。
func(vValue)TryRecv()(xreflect.Value,okbool)//尝试接收数据(不会阻塞),v值必须是可读通道。
func(vValue)Close()//关闭通道
函数相关
func(vValue)Call(in[]Value)(r[]Value)//通过参数列表in调用v值所代表的函数(或方法)。函数的返回值存入r中返回。
//要传入多少参数就在in中存入多少元素。
//Call即可以调用定参函数(参数数量固定),也可以调用变参函数(参数数量可变)。
func(vValue)CallSlice(in[]Value)[]Value//调用变参函数
同样地,我们可以通过reflect.ValueOf
接受任意interface{}类型,并返回对应的动态类型reflect.Value
:
v:=reflect.ValueOf(2)
fmt.Println(v)//2
fmt.Println(v.String())//<intValue>
看一下reflect.ValueOf
的实现代码:
funcValueOf(iinterface{})Value{
ifi==nil{
returnValue{}
}
//TODO:MaybeallowcontentsofaValuetoliveonthestack.
//Fornowwemakethecontentsalwaysescapetotheheap.It
//makeslifeeasierinafewplaces(seechanrecv/mapassign
//commentbelow).
escapes(i)
returnunpackEface(i)
}
//unpackEfaceconvertstheemptyinterfaceitoaValue.
funcunpackEface(iinterface{})Value{
e:=(*emptyInterface)(unsafe.Pointer(&i))
//NOTE:don'treade.worduntilweknowwhetheritisreallyapointerornot.
t:=e.typ
ift==nil{
returnValue{}
}
f:=flag(t.Kind())
ififaceIndir(t){
f|=flagIndir
}
returnValue{t,e.word,f}
}
escapes()涉及栈和堆的对象分配以及逃逸分析,有兴趣的可以看WilliamKennedy写的系列文章: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()
ifnum,ok:=i.(int);ok{//类型断言
fmt.Println(num)
}
但通常在实际场景中,我们其实并不知道原始值的类型,这里就需要利用reflect.Type
和reflect.Value
的方法探索原始值的信息。下面通过一个例子说明:
packagemain
import(
"fmt"
"reflect"
)
typeDogstruct{
Namestring
Ageint
}
func(dog*Dog)Eat(){
fmt.Printf("%siseating.",dog.Name)
}
func(dog*Dog)Run(){
fmt.Printf("%sisrunning.",dog.Name)
}
func(dogDog)Sleep(){
fmt.Printf("%sissleeping.",dog.Name)
}
func(dogDog)Jump(){
fmt.Printf("%sisjumping.",dog.Name)
}
funcmain(){
doggy:=Dog{"doggy",2}
checkFieldAndMethod(doggy)
fmt.Println("")
tommy:=&Dog{"tommy",2}
checkFieldAndMethod(tommy)
}
funccheckFieldAndMethod(inputinterface{}){
inputType:=reflect.TypeOf(input)
fmt.Println("Typeofinputis:",inputType.Name())
inputValue:=reflect.ValueOf(input)
fmt.Println("Valueofinputis:",inputValue)
//如果input原始类型时指针,通过Elem()方法或者Indirect()获取指针指向的值
ifinputValue.Kind()==reflect.Ptr{
inputValue=inputValue.Elem()
//inputValue=reflect.Indirect(inputValue)
fmt.Println("Valueinputpointstois:",inputValue)
}
//使用NumField()得到结构体中字段的数量,遍历得到字段的值Field(i)和类型Field(i).Type()
fori:=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)
}
//获取方法
fori:=0;i<inputType.NumMethod();i++{
m:=inputType.Method(i)
fmt.Printf("%s:%v\n",m.Name,m.Type)
}
}
运行之后输出:
Typeofinputis:Dog
Valueofinputis:{doggy2}
Name:string=doggy
Age:int=2
Jump:func(main.Dog)
Sleep:func(main.Dog)
Typeofinputis:
Valueofinputis:&{tommy2}
Valueinputpointstois:{tommy2}
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()
有些迷惑,搞不清这两个方法的区别,这里总结一下:
//Elemreturnsthevaluethattheinterfacevcontains
//orthatthepointervpointsto.
//Itpanicsifv'sKindisnotInterfaceorPtr.
//ItreturnsthezeroValueifvisnil.
func(vValue)Elem()Value
//Indirectreturnsthevaluethatvpointsto.
//Ifvisanilpointer,IndirectreturnsazeroValue.
//Ifvisnotapointer,Indirectreturnsv.
funcIndirect(vValue)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(vValue)Elem()Value
//Elem()返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装,类似于*操作,此时的Value表示的是Value的元素且可以寻址。
func(vValue)Addr()Value
//Addr()返回一个持有指向v变量地址的指针的Value封装,类似于&操作。
func(vValue)CanAddr()bool
//CanAddr()返回是否可以获取v持有值的指针。可以获取指针的值被称为可寻址的。
func(vValue)CanSet()bool
//CanSet()返回v持有的值是否可以被修改
然而,值得注意的是并不是所有reflect.Value
类型的反射值都可以修改,考虑下面这个例子:
packagemain
import(
"fmt"
"reflect"
)
funcmain(){
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
的,即可取地址的; - 反射类型变量的值来自导出字段。
有一些修改反射类型变量是可寻址的,有一些则不是:
packagemain
import(
"reflect"
"fmt"
)
funcmain(){
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对应的可取地址的反射值。
对于结构体类型变量,如果成员字段没有导出,那么虽然可以被访问,但不能通过反射修改:
packagemain
import(
"fmt"
"reflect"
)
typeDogstruct{
Namestring
Ageint
sexstring
}
funcmain(){
rDog:=reflect.ValueOf(&Dog{}).Elem()
vAge:=rDog.FieldByName("Age")
vAge.SetInt(1)
vSex:=rDog.FieldByName("sex")
vSex.SetString("male")
}
运行出现报错:SetString使用的值来自于一个未导出的字段。
panic:reflect:reflect.Value.SetStringusingvalueobtainedusingunexportedfield
为了能修改这个值,需要将该字段导出。将Dog类型中的sex成员首字母大写即可。
修改可取地址的reflect.Value
持有的变量值,除了可以通过反射的Set系列方法,还可以通过从反射类型变量获取实际值的指针来修改:
packagemain
import(
"reflect"
"fmt"
)
funcmain(){
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(vValue)Call(in[]Value)[]Value
Call
方法使用输入的参数in调用v持有的函数。参数in是反射值对象的切片,即[]reflect.Value
;调用完成时,函数的返回值通过[]reflect.Value
返回。
packagemain
import(
"fmt"
"reflect"
)
funcadd(a,bint)int{
returna+b
}
funcmain(){
//将函数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(vValue)MethodByName(namestring)Value
//返回v的名为name的方法的已绑定(到v的持有值的)状态的函数形式的Value封装。
举例:
packagemain
import(
"fmt"
"reflect"
)
typeDogstruct{
Namestring
Ageint
}
func(dog*Dog)SetName(namestring){
dog.Name=name
}
funcmain(){
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深入理解和示例】
【Goaddressable详解】
本文内容总结:前言,interface(接口),reflect(反射),总结,参考资料,
原文链接:https://www.cnblogs.com/turling/p/11667441.html