介绍性反射是一种机制,它可以看穿结构的组成,并在编译时更新值,而无需知道具体的类型。通过使用反射,我们可以编写能够以统一的方式处理所有类型的代码。甚至是在编写这部分代码时还不存在的类型。一个具体的例子是裂变材料条约。Println()方法,可以打印出我们自定义的结构类型。
虽然,一般来说,不建议在代码中使用反射。反射影响性能,不容易阅读,并且延迟了可以在编译时检查的类型问题在运行时以恐慌的形式显示。这些都是反射的坏处。但是,我认为反思是必须掌握的,原因如下:
许多标准库和第三方库使用反射,尽管公开的接口是封装的,所以你不需要了解反射。但是如果你想深入研究这些库,了解实现,阅读源代码,反射是绕不过去的。例如encoding/json,encoding/XML;如果需要编写一个可以处理所有类型的函数或方法,我们必须使用反射。因为Go的类型数量是无限的,而且类型可以自定义,所以使用类型断言是不可能达到目的的。Go语言标准库reflect提供了反射功能。
反射基于Go的类型系统,与接口密切相关。
首先简单介绍一下界面。Go语言中的接口规定了一组方法,任何定义这组方法类型的变量(也叫实现接口)都可以赋给这个接口的变量。
包main import ‘ fmt ‘ type Animal interface { Speak()} type Cat struct { } func(c Cat)Speak(){ fmt。Println(‘喵’)} type Dog struct { } func(d Dog)Speak(){ fmt . println(‘ bark ‘)} funcmain(){ var a Animal a=cat { } a . Speak()a=Dog { } a . Speak()}在上面的代码中,我们定义了一个Animal接口,它规定了一个方法Speak()。然后定义了两个结构类型,Cat和Dog,都定义了这个方法。这样,我们可以将猫和狗对象分配给动物类型的变量。
一个接口包含两部分:类型和值,即(类型,值)。Type是分配给接口变量的值的类型,value是分配给接口变量的值。如果我们知道接口中存储的变量的类型,我们也可以使用类型断言通过接口变量获得特定类型的值:
type Animal interface { Speak()} type Cat struct { Name string } func(c Cat)Speak(){ fmt。Println(‘喵’)} func main(){ var A animal A=Cat { name : ‘ kitty ‘ } A . speak()c :=a.(Cat)fmt . println(c . name)}在上面的代码中,我们知道Cat对象存储在接口A中,所以可以直接使用类型断言A .(Cat)来获取Cat对象。但是,如果类型断言的类型与实际存储的类型不匹配,就会直接死机。所以在实际开发中,通常会使用另一种类型的断言形式c,ok :=a.(Cat)。如果类型不匹配,该表单不会出现异常,但会通过将第二个返回值设置为false来表明这一点。
有时,一个类型定义了许多方法,而不仅仅是接口同意的方法。通过接口,我们只能调用接口中约定的方法。当然,我们也可以把它的类型断言为另一个接口,然后调用这个接口约定的方法,前提是原对象实现了这个接口:
r io . reader r=new(bytes . buffer)w=r .(io。Writer) io.reader和io.writer是标准库中两个最常用的接口:
//src/io/io.gotype Reader接口{ Read(p []byte) (n int,err error)}type Writer接口{ Write(p []byte) (n int,Err error)}bytes。Buffer实现了这两个接口,所以字节。缓冲对象可以分配给io。读取器变量R,然后R可以被断言为io。因为值存储在接口io中。读取器也实现io。编写器接口。
如果一个接口A包含了另一个接口B的所有方法,那么接口A的变量可以直接赋给B的变量,因为A中存储的值必须实现A约定的所有方法,所以也必须实现B,此时不需要类型断言。例如,一个io。ReadCloser接口在标准库io中定义,这个接口变量可以直接赋给io。读者:
//src/io/io.go类型readcloser接口{readcloser}空接口接口{}是一个特殊的接口,它没有规定任何方法。所有类型值都可以赋给空接口类型的变量,因为它没有方法限制。
尤其重要的是,接口变量之间的类型断言或直接赋值不会改变存储在接口变量中的类型-值对。只是通过不同的接口可以调用的方法不一样。因此,存储在接口变量中的值不能是接口类型。
有了这些接口的基础知识,我们来介绍一下反射。
基本Go语言中的反射函数是由反射包提供的。反射包定义了一个接口反射。类型和结构反映。值,它定义了大量获取类型信息、设置值等的方法。在反射包中,只有类型描述符实现反射。类型接口。因为类型描述符是未导出的类型,所以我们只能获得反射的值。通过反射打字。TypeOf()方法:
package main import(‘ fmt ‘ ‘ reflect ‘)type Cat struct { Name string } func main(){ var f float 64=3.5 t1 :=reflect。type of(f)t . println(t1 . string())c :=cat { name 3360 ‘ kitty ‘ } t 23360=reflect . type of(c)fmt . println(T2 . string())}输出:
浮动64main。CatGo语言是静态类型,每个变量在编译时都有且只能有一个确定的已知类型,即变量的静态类型。静态类型是在变量声明时确定的,不能修改。静态类型为接口类型的接口变量。尽管在运行时可以为它分配不同类型的值,但只有它的内部动态类型和动态值会被更改。它的静态类型从未改变。
反映。TypeOf()方法用于取出接口的动态类型部分,并将其作为reflect返回。键入Wait!上面的代码里好像没有接口类型。
我们来看看reflect的定义。TypeOf():
//src/reflect/Type . go func Type of(I interface {})Type { eface 3360=*(* empty interface)(unsafe . pointer(I))return to Type(eface . typ)}它接受一个接口{}类型参数,所以上面的float64和Cat变量会转换成接口{ },然后传递给方法。反映。TypeOf()方法获取此接口{}中的类型部分。
据此,反思。ValueOf()方法自然是获取接口的value部分,而返回值是reflect的。值类型。根据上面的示例添加以下代码:
V1 :=reflect . value of(f)fmt . println(v1)fmt . println(v1 . string())v 23360=reflect . value of(c)fmt . println(v2)fmt . println(v2 . string())运行输出:
3.5{kitty}因为fmt。Println()将对反射进行特殊处理。值类型并打印其内部值,上面显示的是反映。调用Value.String()方法以获取更多信息。
Get类型如此常见,以至于fmt提供了格式符号%T输出参数类型:
fmt中的类型。Printf(‘%T\n ‘,3) //intGo语言是无限的,可以通过类型定义新的类型。但是类型是有限的,各种枚举都是在reflect包中定义的:
//src/reflect/type . gotype Kind Uint const(Invalid Kind=iota Bool Int 8 Int 16 Int 32 Int 64 Uint 8 Uint 16 Uint 32 Uint 64 Uint ptr float 32 float 64 complex 64 complex 128 array chan func接口映射ptr切片字符串结构不安全指针)共26种,可分类如下:
Type Bool、String和各种数值类型(有符号整数Int/Int8/Int16/Int32/Int64、无符号整数uint/uint 8/uint 16/uint 32/uint 64/uintptr、浮点数Float32/Float64、Complex Complex64/Complex128)复合(aggregate)类型Array和Struct引用类型Chan、Func、ptr、Slice和Map(值类型和引用类型之间没有明显的区别,我们来理解一下含义)接口类型值是无效类型)Go中的所有类型(包括用户定义的类型
例如:
type int int func main(){ var I int var j myint I=int(j)//必须将ti 3360=reflect . type of(I)fmt . println(‘ type of I : ‘,ti . string())TJ 3360=reflect . type of(j)fmt . println(‘ type of j 3360 ‘,TJ . string())fmt . println(‘ type of I 3360 ‘,ti.kind ()) fmt.println(‘)虽然MyInt的底层类型它们之间的赋值必须强制类型转换。但他们是同类。都是int。
代码输出如下:
I : int类型j3360 main类型。Myint类的i: int类的j3360 int反射用法因为反射内容和API比较多,我们结合具体用法来介绍。
透视数据由透视结构组成,需要以下方法:
反思。ValueOf():获取反射的值对象;反思。Value.NumField():从结构的反射值对象中获取该结构的字段数;反思。Value.Field(i):从结构的反射值对象中获取第I个字段的反射值对象;反思。Kind():从反射值对象中获取种类;Reflect.int()/reflect . uint()/reflect . string()/reflect . bool():这些方法从反射的值对象中获取具体类型。示例:
type User struct { Name string Age int marted bool } func inspect struct(u interface { }){ v :=reflect。i :的value of(u)=0;I v . NumField();i { field :=v.Field(i)开关场。种(){例reflect.Int,反映。Int8,反射。Int16,反射。Int32,反映。Int64: fmt。printf(‘ field :% d type :% s value :% d \ n ‘,I,field。类型()。名称(),字段。Int())大小写反映。Uint,反映一下。Uint8,反映。Uint16,反映。Uint32,反映。Uint64: fmt。printf(‘ field :% d type :% s value :% d \ n ‘,I,field。类型()。名称(),字段。Uint())大小写反映。Bool: fmt。printf(‘ field :% d type :% s value :% t \ n ‘,I,field。类型()。名称(),字段。Bool())大小写反映。字符串: fmt。printf(‘ field :% d type :% s value :% q \ n ‘,I,field。类型()。名称(),字段。String()) default: fmt。Printf(‘field:%d未处理的种类:%s\n ‘,I,field。kind())} } } func main(){ u :=User { name : ‘ DJ ‘,Age: 18,Married: true,} inspect struct (u)}通过组合reflect的NumField()和Field()方法,可以遍历结构中的每个字段。值,然后相应地处理每个字段的种类。
有些方法只能在原始对象是某种类型时调用。比如NumField()和Field()方法只能在原对象是结构的情况下调用,否则会死机。
识别出具体类型后,就可以调用反射值对象对应的类型方法来获取具体类型的值,比如上面的field.int()/field . uint()/field . bool()/field . string()。但为了减轻处理负担,Int()/UInt()方法合并了类型,它们只返回对应的范围最大的类型。Int()返回Int64类型,Uint()返回Uint64类型。而Int()/Uint()在内部处理对应的有符号或无符号类型,并以Int64/Uint64的形式返回。下面是reflect.Value.Int()方法的实现:
//src/reflect/Value . go func(v Value)Int()Int 64 { k :=v . kind()p :=v . ptr switch k { Case Int : return Int 64(*(* Int)(p))Case Int 83360 return Int 64(*(* Int 8)(p))Case Int 16: return Int 64(*(* Int 16)(p))Case Int 323360 return Int 64(*(*)value.int ‘,v. kind ()})}以上代码,我们只处理了一小部分种类。在实际开发中,完美的处理需要付出很多努力,尤其是当字段是其他复杂类型甚至包含循环引用时。
此外,我们还可以看穿标准库中的结构以及其中未导出的字段。使用上面定义的inspectStruct()方法:
inspect struct(bytes . buffer { })bytes . buffer的结构如下:
type buffer struct { buf[]byte off int latest read op }是未导出的字段,程序输出:
字段:0未处理的种类:字段:1类型:int值:0字段:2类型:读数值:0透视地图组成,需要以下方法:
反思价值。映射键():将每个键的反思。价值对象组成一个切片返回;反思价值。MapIndex(k):传入键的反思。价值对象,返回值的反思。价值;然后可以对键和值的反思。价值进行和上面一样的处理。示例:
功能检查图(m接口{}) { v :=反射ValueOf(m) for _,k :=range v . map keys(){ field :=v . MapIndex(k)fmt .Printf(‘%v=%v\n ‘,k.Interface(),字段interface())} } func main(){ inspectMap(map[uint 32]uint 32 { 1: 2,3: 4,})}我这里偷懒了,没有针对每个种类去做处理,直接调用键-值反思。价值的接口()方法。该方法以空接口的形式返回内部包含的值。程序输出:
1=23=4同样地,映射键()和地图索引(k)方法只能在原对象是地图类型时才能调用,否则会恐慌。
透视切片或数组组成,需要以下方法:
反思价值。Len():返回数组或切片的长度;反思。价值指数(一):返回第我个元素的反思。价值值;然后对这个反思。价值判断种类()进行处理。示例:
func inspectSliceArray(sa接口{}) { v :=反射.滤波多音的值Printf(‘%c ‘,'[‘)for I :=0;I v . Len();i { elem :=v索引(i) fmtPrintf(‘%v ‘,元素.Interface()) } fmt .Printf(‘%c\n ‘,’]’)} func main(){ inspectSliceArray([]int { 1,2,3}) inspectSliceArray([3]int{4,5,6})}程序输出:
[1 2 3 ][4 5 6 ]同样地Len()和指数(一)方法只能在原对象是切片,数组或字符串时才能调用,其他类型会恐慌。
透视函数类型,需要以下方法:
反思.类型。NumIn():获取函数参数个数;反思。在中键入:获取第我个参数的反思。类型;反思.类型。NumOut():获取函数返回值个数;反思。类型输出:获取第我个返回值的反思。类型。示例:
func Add(a,b int) int { return a b}func问候语(名称字符串)string { return ‘ hello ‘ name } func inspect func(名称字符串,f接口{}) { t :=反射.类型裂变材料条约.i :=0的Println(名称,’输入: ‘);I t . NumIn();i { t :=t.In(i) fmt .Print(t.Name()) fmt .print(‘)} fmt .Println() fmt .对于i :=0,println(‘ output : ‘);I t . NumOut();i { t :=t.Out(i) fmt .Print(t.Name()) fmt .print(‘)} fmt .println(‘ \ n===========’)} func main(){ inspect func(‘ Add ‘,Add) inspectFunc(‘Greeting ‘,Greeting)}同样地,只有在原对象是函数类型的时候才能调用NumIn()/In()/NumOut()/Out()这些方法,其他类型会恐慌。
程序输出:
将输入:int添加到intoutput:int===========问候语输入:字符串输出:字符串========透视结构体中定义的方法,需要以下方法:
反思.类型。NumMethod():返回结构体定义的方法个数;反思。类型。方法:返回第我个方法的反思。方法对象;示例:
函数检查方法(o接口{}) { t :=反射.i :的式中(o)=0;I t . NumMethod();我{ m :=t。方法(I)fmtprintln(m)} } type User struct { Name string Age int } func(u * User)SetName(n string){ u . Name=n } func(u * User)SetAge(a int){ u . Age=a } func main(){ u :=User { Name : ‘ DJ ‘,Age: 18,} inspectMethod(u)}reflect .方法定义如下:
//src/reflect/type.gotype方法结构{名称字符串//方法名PkgPath字符串类型类型//方法类型(即函数类型)函数值//方法值(以接收器作为第一个参数)Index int //是结构体中的第几个方法}事实上,反思。价值也定义了NumMethod()/方法这些方法。区别在于:反思。类型。方法(一)返回的是一个反思。方法对象,可以获取方法名、类型、是结构体中的第几个方法等信息。如果要通过这个反思。方法调用方法,必须使用功能字段,而且要传入接收器的反思。价值作为第一个参数:
米(meter的缩写))函数调用(v,args)而是反映。方法(I)返回一个反射。值对象,它总是接受反射。值调用方法(I)作为接收方对象,不需要额外的输入。并直接使用Call()发起方法调用:
米(meter的缩写))打电话(.args)反映。打字和反思。Value有很多同名的方法,所以在使用它们的时候要注意区分。
要调用函数或方法来调用函数,需要以下方法:
反思。Value.Call():使用reflect。ValueOf()生成每个参数的反射值对象,然后形成切片,传递给Call()方法。Call()方法执行函数调用并返回[] reflect.value .其中每个元素都是原始返回值的反射值对象。示例:
func Add(a,b int)int { return a b } func Greeting(name string)string { return ‘ hello ‘ name } func invoke(f interface { },args.接口{}) { v :=反射。value of(f)argsV :=make([]reflect。Value,0,len(args)) for _,arg :=range args { argsV=append(argsV,reflect。value of(arg))} RETs :=v . Call(argsV)fmt。Println(‘ret:’) for _,Ret :=rangerets { fmt . println(Ret . interface()} } funcmain(){ invoke(add,1,2) invoke (greeting,’ DJ’)}我们封装了一个invoke()方法,用接口{}空接口和接口{}接收函数对象。该函数首先调用反射。ValueOf()方法来获取函数对象的反射值对象。然后,反思。依次为每个参数调用ValueOf(),生成该参数的反射值对象切片。最后调用函数反射值对象的Call()方法,输出返回值。
程序运行结果:
ret:3ret:hello dj方法的调用类似:
type M struct { a,b int Op rune } func(M M)Op()int { switch M . Op { case ‘ ‘ : return M . a M . b case ‘-‘ : return M . a-M . b case ‘ * ‘ : return M . a * M . b case ‘/’ : return M . a/M . b default : panic(‘无效op’) }}func main() { m1 :=M{1,2,’ ‘ } m2
RET :3 RET 3360-1 RET 336030 RET 33604在编译时明确知道方法名的情况下调用上面的方法。如果只给了一个结构对象,通过参数指定调用哪个方法,该怎么办?这需要以下方法:
反思。Value.MethodByName (name):获取反射。结构中定义的命名方法的值对象。默认情况下,该方法有一个receiver参数,即调用methodbyname()方法的reflect.value。示例:
type Math struct { a,b int } func(m Math)Add()int { return m . a m . b } func(m Math)Sub()int { return m . a-m . b } func(m Math)Mul()int { return m . a * m . b } func(m Math)Div()int { return m . a/m . b } func invoke method(obj interface { },name string,args.接口{}) { v :=反射。value of(obj)m :=v . method by name(name)argsV :=make([]reflect。Value,0,len(args)) for _,arg :=range args { argsV=append(argsV,reflect。value of(arg))} RETs :=m . Call(argsV)fmt。Println(‘ret:’) for _,ret :=range rets { fmt。Println(ret。interface())} } func main(){ m :=Math { a : 10,b: 2} invokeMethod(m,’ Add’) invokeMethod(m,Sub’) Invoke Method (m,’ mul’) Invoke Method (m,’ div’)}我们可以在一个结构的反射值对象上使用NumMethod()和Method()来遍历它定义的所有方法。
在实际情况下,我们可以通过使用上述方法轻松实现一个简单的基于HTTP的RPC调用。约定:路径名/obj/method/arg1/arg2调用obj.method(arg1,arg2)方法。
首先,定义两个结构,并为它们定义方法。我们同意将可导出的方法注册为RPC方法。该方法必须返回两个值:一个结果和一个错误。
type string object struct { } func(string object)Concat(S1,s2 string) (string,error) { return s1 s2,nil } func(string object)to upper(s string)(string,error){ return string .ToUpper(s),nil } func(string对象)to lower(s string)(string,error) {返回字符串ToLower(s),nil } type math object struct { } func(math object)Add(a,b int) (int,error) { return a b,nil}func (MathObject) Sub(a,b int) (int,error) { return a – b,nil } func(a,b int) (int,error) { return a * b,nil}func (MathObject) Div(a,b int) (int,error) { if b==0 { return 0,errors .新(‘用零填充)}返回a/b,nil}接下来我们定义一个结构表示可以调用的位置遥控(远程位置控制)方法:
类型RpcMethod struct { method reflect .值参数[]反映。类型}其中方法是方法的反射值对象,参数是各个参数的类型。我们定义一个函数从对象中提取可以位置遥控(远程位置控制)调用的方法:
var(mapObjMethods map[string]map[string]RPC method)func init(){ mapObjMethods=make(map[string]map[string]RPC method)} func register methods(objName string,o interface{}) { v :=reflect .i :的(o)mapObjMethods[objName]=make(map[string]RPC method)=0的值;I v . NumMethod();i { m :=v .方法(i) if m.Type().NumOut()!=2 { //排除不是两个返回值的继续}如果m型().出局(1)。Name()!=’错误’ { //排除第二个返回值不是错误的continue } t :=v.Type().方法(I)方法名:=t . Name if len(方法名)=1 | | strings .ToUpper(methodName[0:1])!=methodName[0:1] { //排除非导出方法continue } type :=make([]reflect .类型,0,1)对于j :=0;j m . Type().NumIn();j { types=append(types,m.Type().in(j))} mapObjMethods[objName][方法名]=RPC method { m,types,} }}registerMethods()函数使用反思价值。NumMethod()和反思。方法(一)从对象中遍历方法,排除掉不是两个返回值的、第二个返回值不是错误的或者非导出的方法。
然后定义一个超文本传送协议(超文本传输协议的缩写)处理器:
函数处理程序(w http .回复作者.请求){零件:=字符串Split(r.URL.Path[1:],’/’) if len(parts) 2 { handleError(w,errors .new(‘ invalid request ‘))return } m :=查找方法(parts[0],parts[1])if m . method。是零(){句柄错误(w,fmt .错误f(‘ no this method :% s in object :% s ‘,parts[0],parts[1]))return } args :=parts[2:]if len(m . args)!=len(argSs) { handleError(w,errors .新(‘不一致的参数数))return } argVs :=make([]reflect .Value,0,1) for i,t :=range m . args { switch t . Kind(){ case reflect .Int: value,_ :=strconv .atoi(argSs[I])argVs=append(argVs,reflect .ValueOf(value))案例反映.String: argVs=append(argVs,reflect .(argSs[I])默认值:句柄错误(w,fmt .Errorf(‘invalid arg type:%s ‘,t . Kind()))ret } } ret :=m . method。call(argVs)err :=ret[1].接口()如果err!=nil { handleError(w,err .(错误))return } response(w,ret[0].Interface())}我们将路径分割得到一个切片,第一个元素为对象名(即数学或字符串),第二个元素为方法名(即加法/减法/乘法/除法等),后面的都是参数。接着,我们查找要调用的方法,根据注册时记录的各个参数的类型将路径中的字符串转换为对应类型。然后调用,检查第二个返回值是否为无可以获知方法调用是否出错。成功调用则返回结果。
最后我们只需要启动一个超文本传送协议(超文本传输协议的缩写)服务器即可:
func main(){ register methods(‘ math ‘,math object { })register methods(‘ string ‘,StringObject{}) mux :=http .新服务器()多路复用器HandleFunc(‘/’,handler) server :=http .服务器{ Addr: ‘:8080 ‘,Handler: mux,} if err :=server .ListenAndServe();呃!=nil { log .致命错误)}}完整代码在开源代码库仓库中。运行:
$ go运行main.go使用curl来验证:
$ curl localhost :8080/math/Add/1/2 { ‘ data ‘ :3 } $ curl localhost :8080/math/Sub/10/2 { ‘ data ‘ :808 } $ curl localhost :8080/math/Div/10/2 { ‘ data ‘ :5 } $ curl localhost :8080/math/Div/10/0 { ‘方法参数的类型目前只支持int和string,有兴趣可以改进一下。
设置值首先引入一个概念:可寻址性。可寻址性是通过反射获得其地址的能力。可寻址性与指针密切相关。所有的反射。通过反射获得的值。的值()不可寻址。因为他们只保留自己的价值观,对自己的地址一无所知。比如指针p *int在内存中保存了另一个int数据的地址,但是它自己的地址是不能自己获取的,因为它自己的地址信息在传递给reflect的时候丢失了。的值()。我们可以通过反射判断它是否可寻址。Value . CanAddr():
func main(){ x :=2 a :=reflect。ValueOf(2) b :=反射。ValueOf(x) c :=反射。(x) fmt的值。println(a . canadr())//True fmt . println(b . canadr())//false fmt . println(c . canadr())//false }虽然指针是不可寻址的,但是我们可以对其反射对象调用Elem()来获取反射。它指向的元素的值。这反映了。价值是可以被寻址的,因为它是通过反思获得的价值。Value.Elem(),并且可以记录该采集路径。因此,它的地址保存在反射中。获得的值:
d :=c . elem()fmt . println(d . canadr())另外,反射。通过对反射对象进行切片的Index(i)方法获得的值也是可寻址的,我们总是可以通过切片获得索引的地址。通过结构指针获得的字段也是可寻址的:
type User struct { Name string Age int } s :=[]int { 1,2,3}sv :=reflect。e的值:=sv。索引(1)fmt。println(e . canadr())//trueu :=User { name : ‘ DJ ‘,Age: 18} uv3360=reflect。Valueof (u) f:=uv.elem()。field(0)fmt . println(f . canadr())//true如果一个reflect.value是可寻址的,我们可以调用它的Addr()方法返回一个reflect。然后在这个反射上调用接口{}方法。值,它将返回包含该指针的接口{}值。如果我们知道类型,我们可以使用类型断言将其转换为普通指针。通过普通指针更新值:
funmain(){ x 3360=2d :=reflect。值(x)。Elem () pX3360=d.addr()。接口()。(* int) * px=3fmt.println (x)//3}这个更新方式有点麻烦。
d . set(reflect . value of(4))fmt . println(x)//4如果传入的类型不匹配,就会死机。反思。Value为基本类型提供了特殊的Set方法:SetInt、setuit、SetFloat等。
D.SetInt(5)fmt。Println(x) //5反射可以读取未导出的结构字段的值,但不能更新它们。可寻址反射。值将记录它是否是通过遍历未导出的字段获得的,如果是,则不允许修改。因此,在更新之前使用CanAddr()判断是不安全的。CanSet()可以正确判断一个值是否可以修改。
CanSet()判断可设置性,这是一个比可寻址性更严格的属性。如果一反映。值是可设置的,它必须是可寻址的。否则,就不是:
type User struct { Name string age int } u :=User { Name : ‘ DJ ‘,age: 18}uv :=reflect。ValueOf(u)name :=uv。Elem()。字段(0)fmt。Println(名称。CanAddr(),名称。CanSet()) //true trueage :=uv。Elem()。字段(1)fmt。Println(年龄。CanAddr(),age . canset()//true false name . setstring(‘李大军’)fmt . println(u)//{李大军18}//error //age。SetInt(20)StructTag定义结构时,可以为每个字段指定一个标记。我们可以通过反射读取这些标签:
type User struct { Name string ` JSON : ‘ Name ‘ ` Age int ` JSON : ‘ Age ‘ `} func main(){ u :=User { Name : ‘ DJ ‘,Age: 18} t :=reflect。类型(u)。i :的Elem()为0;I t . NumField();I { f :=t . field(I)fmt . println(f . tag)} }标签是一个普通的字符串,上面的程序输出:
JSON : ‘ name ‘ JSON 3360 ‘ age ‘ struct标记是在reflect/type.go文件中定义的:
StructTag string类型的一般做法是用空格分隔键值对,并在键值之间使用:例如:
` JSON : ‘ name ‘ XML 3360 ‘ age ‘ ` struct tag提供了Get()方法来获取键的相应值。
本文总结并系统介绍了Go语言中的反射机制,从类型、接口到反射用法。还使用反射实现了一个简单的基于HTTP的RPC库。虽然在正常开发中不建议使用反射,但是在阅读源代码和编写自己的库时,需要经常使用反射知识。掌握反射可以让源代码阅读更加有效。
如果你发现了一个有趣且易于使用的Go语言库,欢迎在GitHub上提交问题,GitHub是一个Go的日常库。
参考Rob Pike,反射定律3360 https://golang.org/doc/articles/laws_of_reflection.htmlGo编程语言,第12章:反射的官方文档,https://pkg.go.dev/reflectGo日报图书馆GitHub:https://github.com/darjun/go-daily-lib
暂无讨论,说说你的看法吧