简介
反射是一种机制,在编译时不晓得具体类型的状况下,能够透视构造的组成、更新值。应用反射,能够让咱们编写出能对立解决所有类型的代码。甚至是编写这部分代码时还不存在的类型。一个具体的例子就是fmt.Println()
办法,能够打印出咱们自定义的构造类型。
尽管,一般来说都不倡议在代码中应用反射。反射影响性能、不易浏览、将编译时就能查看进去的类型问题推延到运行时以 panic 模式体现进去,这些都是反射的毛病。然而,我认为反射是肯定要把握的,起因如下:
- 很多规范库和第三方库都用到了反射,尽管裸露的接口做了封装,不须要理解反射。然而如果要深入研究这些库,理解实现,浏览源码, 反射是绕不过来的。例如
encoding/json
,encoding/xml
等; - 如果有一个需要,编写一个能够解决所有类型的函数或办法,咱们就必须会用到反射。因为 Go 的类型数量是有限的,而且能够自定义类型,所以应用类型断言是无奈达成指标的。
Go 语言规范库reflect
提供了反射性能。
接口
反射是建设在 Go 的类型零碎之上的,并且与接口密切相关。
首先简略介绍一下接口。Go 语言中的接口约定了一组办法汇合,任何定义了这组办法的类型(也称为实现了接口)的变量都能够赋值给该接口的变量。
package mainimport "fmt"type Animal interface { Speak()}type Cat struct {}func (c Cat) Speak() { fmt.Println("Meow")}type Dog struct {}func (d Dog) Speak() { fmt.Println("Bark")}func main() { var a Animal a = Cat{} a.Speak() a = Dog{} a.Speak()}
下面代码中,咱们定义了一个Animal
接口,它约定了一个办法Speak()
。而后定义了两个构造类型Cat
和Dog
,都定义了这个办法。这样,咱们就能够将Cat
和Dog
对象赋值给Animal
类型的变量了。
接口变量蕴含两局部:类型和值,即(type, value)
。类型就是赋值给接口变量的值的类型,值就是赋值给接口变量的值。如果晓得接口中存储的变量类型,咱们也能够应用类型断言通过接口变量获取具体类型的值:
type Animal interface { Speak()}type Cat struct { Name string}func (c Cat) Speak() { fmt.Println("Meow")}func main() { var a Animal a = Cat{Name: "kitty"} a.Speak() c := a.(Cat) fmt.Println(c.Name)}
下面代码中,咱们晓得接口a
中保留的是Cat
对象,间接应用类型断言a.(Cat)
获取Cat
对象。然而,如果类型断言的类型与理论存储的类型不符,会间接 panic。所以理论开发中,通常应用另一种类型断言模式c, ok := a.(Cat)
。如果类型不符,这种模式不会 panic,而是通过将第二个返回值置为 false 来表明这种状况。
有时候,一个类型定义了很多办法,而不只是接口约定的办法。通过接口,咱们只能调用接口中约定的办法。当然咱们也能够将其类型断言为另一个接口,而后调用这个接口约定的办法,前提是原对象实现了这个接口:
var r io.Readerr = new(bytes.Buffer)w = r.(io.Writer)
io.Reader
和io.Writer
是规范库中应用最为频繁的两个接口:
// src/io/io.gotype Reader interface { Read(p []byte) (n int, err error)}type Writer interface { Write(p []byte) (n int, err error)}
bytes.Buffer
同时实现了这两个接口,所以byte.Buffer
对象能够赋值给io.Reader
变量r
,而后r
能够断言为io.Writer
,因为接口io.Reader
中存储的值也实现了io.Writer
接口。
如果一个接口A
蕴含另一个接口B
的所有办法,那么接口A
的变量能够间接赋值给B
的变量,因为A
中存储的值肯定实现了A
约定的所有办法,那么必定也实现了B
。此时,毋庸类型断言。例如规范库io
中还定义了一个io.ReadCloser
接口,此接口变量能够间接赋值给io.Reader
:
// src/io/io.gotype ReadCloser interface { Reader Closer}
空接口interface{}
是比拟非凡的一个接口,它没有约定任何办法。所有类型值都能够赋值给空接口类型的变量,因为它没有任何办法限度。
有一点特地重要,接口变量之间类型断言也好,间接赋值也好,其外部存储的(type, value)
类型-值对是没有变动的。只是通过不同的接口能调用的办法有所不同而已。也是因为这个起因,接口变量中存储的值肯定不是接口类型。
有了这些接口的基础知识,上面咱们介绍反射。
反射根底
Go 语言中的反射性能由reflect
包提供。reflect
包定义了一个接口reflect.Type
和一个构造体reflect.Value
,它们定义了大量的办法用于获取类型信息,设置值等。在reflect
包外部,只有类型描述符实现了reflect.Type
接口。因为类型描述符是未导出类型,咱们只能通过reflect.TypeOf()
办法获取reflect.Type
类型的值:
package mainimport ( "fmt" "reflect")type Cat struct { Name string}func main() { var f float64 = 3.5 t1 := reflect.TypeOf(f) fmt.Println(t1.String()) c := Cat{Name: "kitty"} t2 := reflect.TypeOf(c) fmt.Println(t2.String())}
输入:
float64main.Cat
Go 语言是动态类型的,每个变量在编译期有且只能有一个确定的、已知的类型,即变量的动态类型。动态类型在变量申明的时候就曾经确定了,无奈批改。一个接口变量,它的动态类型就是该接口类型。尽管在运行时能够将不同类型的值赋值给它,扭转的也只是它外部的动静类型和动静值。它的动态类型始终没有扭转。
reflect.TypeOf()
办法就是用来取出接口中的动静类型局部,以reflect.Type
返回。等等!下面代码如同并没有接口类型啊?
咱们看下reflect.TypeOf()
的定义:
// src/reflect/type.gofunc TypeOf(i interface{}) Type { eface := *(*emptyInterface)(unsafe.Pointer(&i)) return toType(eface.typ)}
它承受一个interface{}
类型的参数,所以下面的float64
和Cat
变量会先转为interface{}
再传给办法,reflect.TypeOf()
办法获取的就是这个interface{}
中的类型局部。
相应地,reflect.ValueOf()
办法天然就是获取接口中的值局部,返回值为reflect.Value
类型。在上例根底上增加上面代码:
v1 := reflect.ValueOf(f)fmt.Println(v1)fmt.Println(v1.String())v2 := reflect.ValueOf(c)fmt.Println(v2)fmt.Println(v2.String())
运行输入:
3.5<float64 Value>{kitty}<main.Cat Value>
因为fmt.Println()
会对reflect.Value
类型做非凡解决,打印其外部的值,所以下面显示调用了reflect.Value.String()
办法获取更多信息。
获取类型如此常见,fmt
提供了格式化符号%T
输入参数类型:
fmt.Printf("%T\n", 3) // int
Go 语言中类型是有限的,而且能够通过type
定义新的类型。然而类型的品种是无限的,reflect
包中定义了所有品种的枚举:
// src/reflect/type.gotype Kind uintconst ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer)
一共 26 种,咱们能够分类如下:
- 根底类型
Bool
、String
以及各种数值类型(有符号整数Int/Int8/Int16/Int32/Int64
,无符号整数Uint/Uint8/Uint16/Uint32/Uint64/Uintptr
,浮点数Float32/Float64
,复数Complex64/Complex128
) - 复合(聚合)类型
Array
和Struct
- 援用类型
Chan
、Func
、Ptr
、Slice
和Map
(值类型和援用类型辨别不显著,这里不引战,大家了解意思就行) - 接口类型
Interface
- 非法类型
Invalid
,示意它还没有任何值(reflect.Value
的零值就是Invalid
类型)
Go 中所有的类型(包含自定义的类型),都是下面这些类型或它们的组合。
例如:
type MyInt intfunc main() { var i int var j MyInt i = int(j) // 必须强转 ti := reflect.TypeOf(i) fmt.Println("type of i:", ti.String()) tj := reflect.TypeOf(j) fmt.Println("type of j:", tj.String()) fmt.Println("kind of i:", ti.Kind()) fmt.Println("kind of j:", tj.Kind())}
下面两个变量的动态类型别离为int
和MyInt
,是不同的。尽管MyInt
的底层类型(underlying type)也是int
。它们之间的赋值必须要强制类型转换。然而它们的品种是一样的,都是int
。
代码输入如下:
type of i: inttype of j: main.MyIntkind of i: intkind of j: int
反射用法
因为反射的内容和 API 十分多,咱们联合具体用法来介绍。
透视数据组成
透视构造体组成,须要以下办法:
reflect.ValueOf()
:获取反射值对象;reflect.Value.NumField()
:从构造体的反射值对象中获取它的字段个数;reflect.Value.Field(i)
:从构造体的反射值对象中获取第i
个字段的反射值对象;reflect.Kind()
:从反射值对象中获取品种;reflect.Int()/reflect.Uint()/reflect.String()/reflect.Bool()
:这些办法从反射值对象做取出具体类型。
示例:
type User struct { Name string Age int Married bool}func inspectStruct(u interface{}) { v := reflect.ValueOf(u) for i := 0; i < v.NumField(); i++ { field := v.Field(i) switch field.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: fmt.Printf("field:%d type:%s value:%d\n", i, field.Type().Name(), field.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: fmt.Printf("field:%d type:%s value:%d\n", i, field.Type().Name(), field.Uint()) case reflect.Bool: fmt.Printf("field:%d type:%s value:%t\n", i, field.Type().Name(), field.Bool()) case reflect.String: fmt.Printf("field:%d type:%s value:%q\n", i, field.Type().Name(), field.String()) default: fmt.Printf("field:%d unhandled kind:%s\n", i, field.Kind()) } }}func main() { u := User{ Name: "dj", Age: 18, Married: true, } inspectStruct(u)}
联合应用reflect.Value
的NumField()
和Field()
办法能够遍历构造体的每个字段。而后针对每个字段的Kind
做相应的解决。
有些办法只有在原对象是某种特定类型时,能力调用。例如NumField()
和Field()
办法只有原对象是构造体时能力调用,否则会panic
。
辨认出具体类型后,能够调用反射值对象的对应类型办法获取具体类型的值,例如下面的field.Int()/field.Uint()/field.Bool()/field.String()
。然而为了加重解决的累赘,Int()/Uint()
办法对品种做了合并解决,它们只返回相应的最大范畴的类型,Int()
返回Int64
类型,Uint()
返回Uint64
类型。而Int()/Uint()
外部会对相应的有符号或无符号品种做解决,转为Int64/Uint64
返回。上面是reflect.Value.Int()
办法的实现:
// src/reflect/value.gofunc (v Value) Int() int64 { k := v.kind() p := v.ptr switch k { case Int: return int64(*(*int)(p)) case Int8: return int64(*(*int8)(p)) case Int16: return int64(*(*int16)(p)) case Int32: return int64(*(*int32)(p)) case Int64: return *(*int64)(p) } panic(&ValueError{"reflect.Value.Int", v.kind()})}
下面代码,咱们只解决了少部分品种。在理论开发中,欠缺的解决须要花费一番功夫,特地是字段是其余简单类型,甚至蕴含循环援用的时候。
另外,咱们也能够透视规范库中的构造体,并且能够透视其中的未导出字段。应用下面定义的inspectStruct()
办法:
inspectStruct(bytes.Buffer{})
bytes.Buffer
的构造如下:
type Buffer struct { buf []byte off int lastRead readOp}
都是未导出的字段,程序输入:
field:0 unhandled kind:slicefield:1 type:int value:0field:2 type:readOp value:0
透视map
组成,须要以下办法:
reflect.Value.MapKeys()
:将每个键的reflect.Value
对象组成一个切片返回;reflect.Value.MapIndex(k)
:传入键的reflect.Value
对象,返回值的reflect.Value
;- 而后能够对键和值的
reflect.Value
进行和下面一样的解决。
示例:
func inspectMap(m interface{}) { v := reflect.ValueOf(m) for _, k := range v.MapKeys() { field := v.MapIndex(k) fmt.Printf("%v => %v\n", k.Interface(), field.Interface()) }}func main() { inspectMap(map[uint32]uint32{ 1: 2, 3: 4, })}
我这里偷懒了,没有针对每个Kind
去做解决,间接调用键-值reflect.Value
的Interface()
办法。该办法以空接口的模式返回外部蕴含的值。程序输入:
1 => 23 => 4
同样地,MapKeys()
和MapIndex(k)
办法只能在原对象是map
类型时能力调用,否则会panic
。
透视切片或数组组成,须要以下办法:
reflect.Value.Len()
:返回数组或切片的长度;reflect.Value.Index(i)
:返回第i
个元素的reflect.Value
值;- 而后对这个
reflect.Value
判断Kind()
进行解决。
示例:
func inspectSliceArray(sa interface{}) { v := reflect.ValueOf(sa) fmt.Printf("%c", '[') for i := 0; i < v.Len(); i++ { elem := v.Index(i) fmt.Printf("%v ", elem.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()
和Index(i)
办法只能在原对象是切片,数组或字符串时能力调用,其余类型会panic
。
透视函数类型,须要以下办法:
reflect.Type.NumIn()
:获取函数参数个数;reflect.Type.In(i)
:获取第i
个参数的reflect.Type
;reflect.Type.NumOut()
:获取函数返回值个数;reflect.Type.Out(i)
:获取第i
个返回值的reflect.Type
。
示例:
func Add(a, b int) int { return a + b}func Greeting(name string) string { return "hello " + name}func inspectFunc(name string, f interface{}) { t := reflect.TypeOf(f) fmt.Println(name, "input:") for i := 0; i < t.NumIn(); i++ { t := t.In(i) fmt.Print(t.Name()) fmt.Print(" ") } fmt.Println() fmt.Println("output:") for i := 0; i < t.NumOut(); i++ { t := t.Out(i) fmt.Print(t.Name()) fmt.Print(" ") } fmt.Println("\n===========")}func main() { inspectFunc("Add", Add) inspectFunc("Greeting", Greeting)}
同样地,只有在原对象是函数类型的时候能力调用NumIn()/In()/NumOut()/Out()
这些办法,其余类型会panic
。
程序输入:
Add input:int intoutput:int===========Greeting input:stringoutput:string===========
透视构造体中定义的办法,须要以下办法:
reflect.Type.NumMethod()
:返回构造体定义的办法个数;reflect.Type.Method(i)
:返回第i
个办法的reflect.Method
对象;
示例:
func inspectMethod(o interface{}) { t := reflect.TypeOf(o) for i := 0; i < t.NumMethod(); i++ { m := t.Method(i) fmt.Println(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.Method
定义如下:
// src/reflect/type.gotype Method struct { Name string // 办法名 PkgPath string Type Type // 办法类型(即函数类型) Func Value // 办法值(以接收器作为第一个参数) Index int // 是构造体中的第几个办法}
事实上,reflect.Value
也定义了NumMethod()/Method(i)
这些办法。区别在于:reflect.Type.Method(i)
返回的是一个reflect.Method
对象,能够获取办法名、类型、是构造体中的第几个办法等信息。如果要通过这个reflect.Method
调用办法,必须应用Func
字段,而且要传入接收器的reflect.Value
作为第一个参数:
m.Func.Call(v, ...args)
然而reflect.Value.Method(i)
返回一个reflect.Value
对象,它总是以调用Method(i)
办法的reflect.Value
作为接收器对象,不须要额定传入。而且间接应用Call()
发动办法调用:
m.Call(...args)
reflect.Type
和reflect.Value
有不少同名办法,应用时须要留神甄别。
调用函数或办法
调用函数,须要以下办法:
reflect.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 ...interface{}) { v := reflect.ValueOf(f) argsV := make([]reflect.Value, 0, len(args)) for _, arg := range args { argsV = append(argsV, reflect.ValueOf(arg)) } rets := v.Call(argsV) fmt.Println("ret:") for _, ret := range rets { fmt.Println(ret.Interface()) }}func main() { invoke(Add, 1, 2) invoke(Greeting, "dj")}
咱们封装一个invoke()
办法,以interface{}
空接口接管函数对象,以interface{}
可变参数接管函数调用的参数。函数外部首先调用reflect.ValueOf()
办法取得函数对象的反射值对象。而后顺次对每个参数调用reflect.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("invalid op") }}func main() { m1 := M{1, 2, '+'} m2 := M{3, 4, '-'} m3 := M{5, 6, '*'} m4 := M{8, 2, '/'} invoke(m1.Op) invoke(m2.Op) invoke(m3.Op) invoke(m4.Op)}
运行后果:
ret:3ret:-1ret:30ret:4
以上是在编译期明确晓得办法名的状况下发动调用。如果只给一个构造体对象,通过参数指定具体调用哪个办法该怎么做呢?这须要以下办法:
reflect.Value.MethodByName(name)
:获取构造体中定义的名为name
的办法的reflect.Value
对象,这个办法默认有接收器参数,即调用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 invokeMethod(obj interface{}, name string, args ...interface{}) { v := reflect.ValueOf(obj) m := v.MethodByName(name) argsV := make([]reflect.Value, 0, len(args)) for _, arg := range args { argsV = append(argsV, reflect.ValueOf(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") invokeMethod(m, "Mul") invokeMethod(m, "Div")}
咱们能够在构造体的反射值对象上应用NumMethod()
和Method()
遍历它定义的所有办法。
实战案例
应用后面介绍的办法,咱们很容易实现一个简略的、基于 HTTP 的 RPC 调用。约定格局:路径名/obj/method/arg1/arg2
调用obj.method(arg1, arg2)
办法。
首先定义两个构造体,并为它们定义方法,咱们约定可导出的办法会注册为 RPC 办法。并且办法必须返回两个值:一个后果,一个谬误。
type StringObject struct{}func (StringObject) Concat(s1, s2 string) (string, error) { return s1 + s2, nil}func (StringObject) ToUpper(s string) (string, error) { return strings.ToUpper(s), nil}func (StringObject) ToLower(s string) (string, error) { return strings.ToLower(s), nil}type MathObject struct{}func (MathObject) Add(a, b int) (int, error) { return a + b, nil}func (MathObject) Sub(a, b int) (int, error) { return a - b, nil}func (MathObject) Mul(a, b int) (int, error) { return a * b, nil}func (MathObject) Div(a, b int) (int, error) { if b == 0 { return 0, errors.New("divided by zero") } return a / b, nil}
接下来咱们定义一个构造示意能够调用的 RPC 办法:
type RpcMethod struct { method reflect.Value args []reflect.Type}
其中method
是办法的反射值对象,args
是各个参数的类型。咱们定义一个函数从对象中提取能够 RPC 调用的办法:
var ( mapObjMethods map[string]map[string]RpcMethod)func init() { mapObjMethods = make(map[string]map[string]RpcMethod)}func registerMethods(objName string, o interface{}) { v := reflect.ValueOf(o) mapObjMethods[objName] = make(map[string]RpcMethod) for i := 0; i < v.NumMethod(); i++ { m := v.Method(i) if m.Type().NumOut() != 2 { // 排除不是两个返回值的 continue } if m.Type().Out(1).Name() != "error" { // 排除第二个返回值不是 error 的 continue } t := v.Type().Method(i) methodName := t.Name if len(methodName) <= 1 || strings.ToUpper(methodName[0:1]) != methodName[0:1] { // 排除非导出办法 continue } types := make([]reflect.Type, 0, 1) for j := 0; j < m.Type().NumIn(); j++ { types = append(types, m.Type().In(j)) } mapObjMethods[objName][methodName] = RpcMethod{ m, types, } }}
registerMethods()
函数应用reflect.Value.NumMethod()
和reflect.Method(i)
从对象中遍历办法,排除掉不是两个返回值的、第二个返回值不是 error 的或者非导出的办法。
而后定义一个 http 处理器:
func handler(w http.ResponseWriter, r *http.Request) { parts := strings.Split(r.URL.Path[1:], "/") if len(parts) < 2 { handleError(w, errors.New("invalid request")) return } m := lookupMethod(parts[0], parts[1]) if m.method.IsZero() { handleError(w, fmt.Errorf("no such method:%s in object:%s", parts[0], parts[1])) return } argSs := parts[2:] if len(m.args) != len(argSs) { handleError(w, errors.New("inconsistant args num")) 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)) case reflect.String: argVs = append(argVs, reflect.ValueOf(argSs[i])) default: handleError(w, fmt.Errorf("invalid arg type:%s", t.Kind())) return } } ret := m.method.Call(argVs) err := ret[1].Interface() if err != nil { handleError(w, err.(error)) return } response(w, ret[0].Interface())}
咱们将门路宰割失去一个切片,第一个元素为对象名(即math
或string
),第二个元素为办法名(即Add/Sub/Mul/Div
等),前面的都是参数。接着,咱们查找要调用的办法,依据注册时记录的各个参数的类型将门路中的字符串转换为对应类型。而后调用,查看第二个返回值是否为nil
能够获知办法调用是否出错。胜利调用则返回后果。
最初咱们只须要启动一个 http 服务器即可:
func main() { registerMethods("math", MathObject{}) registerMethods("string", StringObject{}) mux := http.NewServeMux() mux.HandleFunc("/", handler) server := &http.Server{ Addr: ":8080", Handler: mux, } if err := server.ListenAndServe(); err != nil { log.Fatal(err) }}
残缺代码在 Github 仓库中。运行:
$ go run main.go
应用 curl 来验证:
$ curl localhost:8080/math/Add/1/2{"data":3}$ curl localhost:8080/math/Sub/10/2{"data":8}$ curl localhost:8080/math/Div/10/2{"data":5}$ curl localhost:8080/math/Div/10/0{"error":"divided by zero"}$ curl localhost:8080/string/Concat/abc/def{"data":"abcdef"}
当然,这只是一个简略的实现,还有很多错误处理没有思考,办法参数的类型目前只反对int
和string
,感兴趣能够去欠缺一下。
设置值
首先介绍一个概念:可寻址。可寻址是能够通过反射取得其地址的能力。可寻址与指针严密相干。所有通过reflect.ValueOf()
失去的reflect.Value
都不可寻址。因为它们只保留了本身的值,对本身的地址无所不知。例如指针p *int
保留了另一个int
数据在内存中的地址,然而它本身的地址无奈通过本身获取到,因为在将它传给reflect.ValueOf()
时,其本身地址信息就失落了。咱们能够通过reflect.Value.CanAddr()
判断是否可寻址:
func main() { x := 2 a := reflect.ValueOf(2) b := reflect.ValueOf(x) c := reflect.ValueOf(&x) fmt.Println(a.CanAddr()) // false fmt.Println(b.CanAddr()) // false fmt.Println(c.CanAddr()) // false}
尽管指针不可寻址,然而咱们能够在其反射对象上调用Elem()
获取它指向的元素的reflect.Value
。这个reflect.Value
就能够寻址了,因为是通过reflect.Value.Elem()
获取的值,能够记录这个获取门路。因此失去的reflect.Value
中保留了它的地址:
d := c.Elem()fmt.Println(d.CanAddr())
另外通过切片反射对象的Index(i)
办法失去的reflect.Value
也是可寻址的,咱们总是能够通过切片失去某个索引的地址。通过构造体的指针获取到的字段也是可寻址的:
type User struct { Name string Age int}s := []int{1, 2, 3}sv := reflect.ValueOf(s)e := sv.Index(1)fmt.Println(e.CanAddr()) // trueu := &User{Name: "dj", Age: 18}uv := reflect.ValueOf(u)f := uv.Elem().Field(0)fmt.Println(f.CanAddr()) // true
如果一个reflect.Value
可寻址,咱们能够调用其Addr()
办法返回一个reflect.Value
,蕴含一个指向原始数据的指针。而后在这个reflect.Value
上调用Interface{}
办法,会返回一个蕴含这个指针的interface{}
值。如果咱们晓得类型,能够应用类型断言将其转为一个一般指针。通过一般指针来更新值:
func main() { x := 2 d := reflect.ValueOf(&x).Elem() px := d.Addr().Interface().(*int) *px = 3 fmt.Println(x) // 3}
这样的更新办法有点麻烦,咱们能够间接通过可寻址的reflect.Value
调用Set()
办法来更新,不必通过指针:
d.Set(reflect.ValueOf(4))fmt.Println(x) // 4
如果传入的类型不匹配,会 panic。reflect.Value
为根本类型提供非凡的Set
办法:SetInt
、SetUint
、SetFloat
等:
d.SetInt(5)fmt.Println(x) // 5
反射能够读取未导出构造字段的值,然而不能更新这些值。一个可寻址的reflect.Value
会记录它是否是通过遍历一个未导出字段来取得的,如果是则不容许批改。所以在更新前应用CanAddr()
判断并不保险。CanSet()
能够正确判断一个值是否能够批改。
CanSet()
判断的是可设置性,它是比可寻址性更严格的性质。如果一个reflect.Value
是可设置的,它肯定是可寻址的。反之则不然:
type User struct { Name string age int}u := &User{Name: "dj", age: 18}uv := reflect.ValueOf(u)name := uv.Elem().Field(0)fmt.Println(name.CanAddr(), name.CanSet()) // true trueage := uv.Elem().Field(1)fmt.Println(age.CanAddr(), age.CanSet()) // true falsename.SetString("lidajun")fmt.Println(u) // &{lidajun 18}// 报错// 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.TypeOf(u).Elem() for i := 0; i < t.NumField(); i++ { f := t.Field(i) fmt.Println(f.Tag) }}
标签就是一个一般的字符串,下面程序输入:
json:"name"json:"age"
StructTag
定义在reflect/type.go
文件中:
type StructTag string
个别常规是将各个键值对,应用空格离开,键值之间应用:
。例如:
`json:"name" xml:"age"`
StructTag
提供Get()
办法获取键对应的值。
总结
本文系统地介绍了 Go 语言中的反射机制,从类型、接口到反射用法。还应用反射实现了一个简略的基于 HTTP 的 RPC 库。反射尽管在平时开发中不倡议应用,然而浏览源码,本人编写库的时候须要频繁用到反射常识。熟练掌握反射能够使源码浏览事倍功半。
大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue
参考
- Rob Pike, laws of reflection: https://golang.org/doc/articles/laws_of_reflection.html
- Go 程序设计语言,第 12 章:反射
- reflect 官网文档,https://pkg.go.dev/reflect
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~