60.Go反射库reflect

文章目录

  • 一、简介
  • 二、前置总结
  • 三、接口
  • 四、反射基础
  • 五、反射用法
    • 1、透视数据组成
    • 2、调用函数或方法
    • 3、设置值
    • 4、StructTag
  • 六、实战案例

一、简介

反射是一种机制,在编译时不知道具体类型的情况下,可以透视结构的组成、更新值。使用反射,可以让我们编写出能统一处理所有类型的代码。甚至是编写这部分代码时还不存在的类型。一个具体的例子就是fmt.Println()方法,可以打印出我们自定义的结构类型。

虽然,一般来说都不建议在代码中使用反射。反射影响性能、不易阅读、将编译时就能检查出来的类型问题推迟到运行时以 panic 形式表现出来,这些都是反射的缺点。但是,我认为反射是一定要掌握的,原因如下:

  • 很多标准库和第三方库都用到了反射,虽然暴露的接口做了封装,不需要了解反射。但是如果要深入研究这些库,了解实现,阅读源码, 反射是绕不过去的。例如encoding/jsonencoding/xml等;我们之前介绍的cast库54.Go类型转换库cast中就大量用到了反射。
  • 如果有一个需求,编写一个可以处理所有类型的函数或方法,我们就必须会用到反射,而且如果是做基础建设能力的工作,由于是做通用底层能力,很多场景都是用到反射的。因为 Go 的类型数量是无限的,而且可以自定义类型,所以使用类型断言是无法达成目标的。

Go 语言标准库reflect提供了反射功能。

二、前置总结

由于本节涉及的反射知识较多,所以就几点反射最基本的信息进行前置总结,避免对后续代码理解产生偏差

  • reflect.TypeOf(v) 返回reflect.Type接口,拿到的是v的类型信息
  • reflect.ValueOf(v)返回reflect.Value结构体,拿到的是v的值信息
  • reflect.Type接口reflect.Value结构体有不少同名方法,使用时需要注意甄别。
  • 注意种类和类型的区别,Go 语言中类型是无限的,而且可以通过type定义新的类型。但是类型的种类是有限的,reflect包中定义了所有种类的枚举:
// src/reflect/type.go
type Kind uintconst (Invalid Kind = iotaBoolIntInt8Int16Int32Int64UintUint8Uint16Uint32Uint64UintptrFloat32Float64Complex64Complex128ArrayChanFuncInterfaceMapPtrSliceStringStructUnsafePointer
)

一共 26 种,我们可以分类如下:

  • 基础类型Bool、String以及各种数值类型(有符号整数Int/Int8/Int16/Int32/Int64,无符号整数Uint/Uint8/Uint16/Uint32/Uint64/Uintptr,浮点数Float32/Float64,复数Complex64/Complex128
  • 复合(聚合)类型ArrayStruct
  • 引用类型Chan、Func、Ptr、SliceMap(值类型和引用类型区分不明显,这里不引战,大家理解意思就行)
  • 接口类型Interface
  • 非法类型Invalid,表示它还没有任何值(reflect.Value的零值就是Invalid类型

Go 中所有的类型(包括自定义的类型),都是上面这些类型或它们的组合。

例如:

type MyInt intfunc main() {var i intvar j MyInti = 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())
}

上面两个变量的静态类型分别为intMyInt,是不同的。虽然MyInt的底层类型(underlying type)也是int。它们之间的赋值必须要强制类型转换。但是它们的种类是一样的,都是int

代码输出如下:

type of i: int
type of j: main.MyInt
kind of i: int
kind of j: int

再来看一个例子,加深对TypeOfValueOf以及他们各自的Kind方法的理解,注意看注释

package mainimport ("fmt""reflect"
)func main() {a := 10b := &a// a的类型是 intfmt.Println(reflect.TypeOf(a))// a的类型的种类是 intfmt.Println(reflect.TypeOf(a).Kind())// a的值是10fmt.Println(reflect.ValueOf(a))// a的值的种类是intfmt.Println(reflect.ValueOf(a).Kind())// b的类型是 *intfmt.Println(reflect.TypeOf(b))// b的类型的种类是 ptrfmt.Println(reflect.TypeOf(b).Kind())// b的值是 0xc00001e080 (一个地址)fmt.Println(reflect.ValueOf(b))// b的值的种类是 ptrfmt.Println(reflect.ValueOf(b).Kind())// b指针所指向的值是 10fmt.Println(reflect.ValueOf(b).Elem())// b指针指向的值的种类为intfmt.Println(reflect.ValueOf(b).Elem().Kind())
}

输出

int
int
10
int
*int
ptr
0xc00001e080
ptr
10
int

三、接口

接口
反射是建立在 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 Animala = Cat{}a.Speak()a = Dog{}a.Speak()
}

上面代码中,我们定义了一个Animal接口,它约定了一个方法Speak()。而后定义了两个结构类型CatDog,都定义了这个方法。这样,我们就可以将CatDog对象赋值给Animal类型的变量了。

接口变量包含两部分:类型和值,即(type, value)类型就是赋值给接口变量的值的类型,值就是赋值给接口变量的值。如果知道接口中存储的变量类型,我们也可以使用类型断言通过接口变量获取具体类型的值:

type Animal interface {Speak()
}type Cat struct {Name string
}func (c Cat) Speak() {fmt.Println("Meow")
}func main() {var a Animala = Cat{Name: "kitty"}a.Speak()c := a.(Cat)fmt.Println(c.Name)fmt.Println()fmt.Println()fmt.Println(reflect.TypeOf(a))fmt.Println(reflect.TypeOf(a).Kind())fmt.Println(reflect.ValueOf(a))fmt.Println(reflect.ValueOf(a).Kind())
}

输出:

Meow
kittymain.Cat
struct
{kitty}
struct

上面代码中,我们知道接口a中保存的是Cat对象,直接使用类型断言a.(Cat)获取Cat对象。但是,如果类型断言的类型与实际存储的类型不符,会直接 panic。所以实际开发中,通常使用另一种类型断言形式c, ok := a.(Cat)。如果类型不符,这种形式不会 panic,而是通过将第二个返回值置为 false 来表明这种情况。

有时候,一个类型定义了很多方法,而不只是接口约定的方法。通过接口,我们只能调用接口中约定的方法。当然我们也可以将其类型断言为另一个接口,然后调用这个接口约定的方法,前提是原对象实现了这个接口:

var r io.Reader
r = new(bytes.Buffer)
w = r.(io.Writer)

io.Readerio.Writer是标准库中使用最为频繁的两个接口:

// src/io/io.go
type 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.go
type ReadCloser interface {ReaderCloser
}

空接口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.5t1 := reflect.TypeOf(f)fmt.Println(t1.String())c := Cat{Name: "kitty"}t2 := reflect.TypeOf(c)fmt.Println(t2.String())
}

输出:

float64
main.Cat

Go 语言是静态类型的,每个变量在编译期有且只能有一个确定的、已知的类型,即变量的静态类型。静态类型在变量声明的时候就已经确定了,无法修改。一个接口变量,它的静态类型就是该接口类型。虽然在运行时可以将不同类型的值赋值给它,改变的也只是它内部的动态类型和动态值。它的静态类型始终没有改变。

reflect.TypeOf()方法就是用来取出接口中的动态类型部分,以reflect.Type返回。等等!上面代码好像并没有接口类型啊?

我们看下reflect.TypeOf()的定义:

// src/reflect/type.go
func TypeOf(i interface{}) Type {eface := *(*emptyInterface)(unsafe.Pointer(&i))return toType(eface.typ)
}

它接受一个interface{}类型的参数,所以上面的float64Cat变量会先转为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())

运行输出:注意其中的<type,value>形式,即main.Cat表示v2是类型,Value表示值v2reflect.Value类型

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.go
type Kind uintconst (Invalid Kind = iotaBoolIntInt8Int16Int32Int64UintUint8Uint16Uint32Uint64UintptrFloat32Float64Complex64Complex128ArrayChanFuncInterfaceMapPtrSliceStringStructUnsafePointer
)

一共 26 种,我们可以分类如下:

  • 基础类型BoolString以及各种数值类型(有符号整数Int/Int8/Int16/Int32/Int64,无符号整数Uint/Uint8/Uint16/Uint32/Uint64/Uintptr,浮点数Float32/Float64,复数Complex64/Complex128
  • 复合(聚合)类型ArrayStruct
  • 引用类型Chan、Func、Ptr、SliceMap(值类型和引用类型区分不明显,这里不引战,大家理解意思就行)
  • 接口类型Interface
  • 非法类型Invalid,表示它还没有任何值(reflect.Value的零值就是Invalid类型)

Go 中所有的类型(包括自定义的类型),都是上面这些类型或它们的组合。

例如:

type MyInt intfunc main() {var i intvar j MyInti = 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())
}

上面两个变量的静态类型分别为intMyInt,是不同的。虽然MyInt的底层类型(underlying type)也是int。它们之间的赋值必须要强制类型转换。但是它们的种类是一样的,都是int

代码输出如下:

type of i: int
type of j: main.MyInt
kind of i: int
kind of j: int

五、反射用法

由于反射的内容和 API 非常多,我们结合具体用法来介绍。

1、透视数据组成

透视结构体组成,需要以下方法:

  • reflect.ValueOf():获取反射值对象,返回reflect.Value类型;
    - reflect.Value.NumField():从结构体的反射值对象中获取它的字段个数,如果reflect.Value不是struct类型,则调用NumFieldpanic
  • reflect.Value.Field(i):从结构体的反射值对象中获取第i个字段的反射值对象,同上,只能用于struct
  • reflect.Kind():从反射值对象中获取种类;
  • reflect.Int()/reflect.Uint()/reflect.String()/reflect.Bool():这些方法从反射值对象中取出具体类型。

示例:

type User struct {Name    stringAge     intMarried bool
}func inspectStruct(u interface{}) {v := reflect.ValueOf(u)  // 返回一个reflect.Value结构体类型for i := 0; i < v.NumField(); i++ {field := v.Field(i)   // 返回一个reflect.Value结构体类型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.ValueNumField()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.go
func (v Value) Int() int64 {k := v.kind()p := v.ptrswitch 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      []byteoff      int   lastRead readOp
}

都是未导出的字段,程序输出:

field:0 unhandled kind:slice
field:1 type:int value:0
field: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) // 获取到reflect.Value 假定kind是mapfor _, k := range v.MapKeys() { // v.MapKeys返回每个键的reflect.Value切片field := v.MapIndex(k) // 通过key的reflect.Value获取到map中值的reflect.Value对象fmt.Printf("%v => %v\n", k.Interface(), field.Interface())}
}func main() {inspectMap(map[uint32]uint32{1: 2,3: 4,})
}

我这里偷懒了,没有针对每个Kind去做处理,直接调用键-值reflect.ValueInterface()方法。该方法以空接口的形式返回内部包含的值。程序输出:

1 => 2
3 => 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 int
output:
int
===========
Greeting input:
string
output:
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    stringAge     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.go
type Method struct {Name    string // 方法名PkgPath stringType  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.Typereflect.Value有不少同名方法,使用时需要注意甄别。

2、调用函数或方法

调用函数,需要以下方法:

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)// 获取每个参数的reflect.Value对象,组成切片,传给call方法argsV := make([]reflect.Value, 0, len(args))for _, arg := range args {argsV = append(argsV, reflect.ValueOf(arg))}// rets中存储的是原来v函数的返回值对应的reflect.Value对象组成的切片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:
3
ret:
hello dj

方法的调用也是类似的:

type M struct {a, b intop   rune
}func (m M) Op() int {switch m.op {case '+':return m.a + m.bcase '-':return m.a - m.bcase '*':return m.a * m.bcase '/':return m.a / m.bdefault: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:
3
ret:
-1
ret:
30
ret:
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()遍历它定义的所有方法。

3、设置值

首先介绍一个概念:可寻址。可寻址是可以通过反射获得其地址的能力。可寻址与指针紧密相关。所有通过reflect.ValueOf()得到的reflect.Value都不可寻址。因为它们只保存了自身的值,对自身的地址一无所知。例如指针p *int保存了另一个int数据在内存中的地址,但是它自身的地址无法通过自身获取到,因为在将它传给reflect.ValueOf()时,其自身地址信息就丢失了。我们可以通过reflect.Value.CanAddr()判断是否可寻址:

func main() {x := 2a := reflect.ValueOf(2)b := reflect.ValueOf(x)c := reflect.ValueOf(&x)fmt.Println(a.CanAddr()) // falsefmt.Println(b.CanAddr()) // falsefmt.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 stringAge  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 := 2d := reflect.ValueOf(&x).Elem()px := d.Addr().Interface().(*int)*px = 3fmt.Println(x) // 3
}

这样的更新方法有点麻烦,我们可以直接通过可寻址的reflect.Value调用Set()方法来更新,不用通过指针:

d.Set(reflect.ValueOf(4))
fmt.Println(x) // 4

如果传入的类型不匹配,会 panicreflect.Value为基本类型提供特殊的Set方法:SetInt、SetUint、SetFloat等:

d.SetInt(5)
fmt.Println(x) // 5

反射可以读取未导出结构字段的值,但是不能更新这些值。一个可寻址的reflect.Value会记录它是否是通过遍历一个未导出字段来获得的,如果是则不允许修改。所以在更新前使用CanAddr()判断并不保险。CanSet()可以正确判断一个值是否可以修改。

CanSet()判断的是可设置性,它是比可寻址性更严格的性质。如果一个reflect.Value是可设置的,它一定是可寻址的。反之则不然:

type User struct {Name string  // Name 是可导出的age  int // age是不可导出的,所以是不可设置的
}u := &User{Name: "dj", age: 18}
uv := reflect.ValueOf(u)
name := uv.Elem().Field(0)
fmt.Println(name.CanAddr(), name.CanSet()) // true true
age := uv.Elem().Field(1)
fmt.Println(age.CanAddr(), age.CanSet()) // true falsename.SetString("lidajun")
fmt.Println(u) // &{lidajun 18}
// 报错
// age.SetInt(20)

4、StructTag

在定义结构体时,可以为每个字段指定一个标签,我们可以使用反射读取这些标签:

type User struct {Name string `json:"name"`Age  int    `json:"age"`
}func main() {u := &User{Name: "dj", Age: 18}// u 是指针类型,所以继续使用Elem得到地址指向的值t := reflect.TypeOf(u).Elem()fmt.Println(reflect.TypeOf(u)) // *main.Userfmt.Println(reflect.TypeOf(u).Elem()) // main.Userfor i := 0; i < t.NumField(); i++ {f := t.Field(i)fmt.Println(f.Tag)fmt.Println(f.Tag.Get("json"))}
}

标签就是一个普通的字符串,上面程序输出:

*main.User
main.User
json:"name"
name
json:"age"
age

如果指针种类(kind)不用Elem(),就使用其他相关方法,会panic,如下
在这里插入图片描述

StructTag定义在reflect/type.go文件中:

type StructTag string

一般惯例是将各个键值对,使用空格分开,键值之间使用:。例如:

`json:"name" xml:"age"`

StructTag提供Get()方法获取键对应的值。

六、实战案例

使用前面介绍的方法,我们很容易实现一个简单的、基于HTTPRPC 调用。约定格式:路径名/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.Valueargs   []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)   // 这里用的是reflect.Value.Method,是为了后续方便调用if m.Type().NumOut() != 2 {// 排除不是两个返回值的continue}if m.Type().Out(1).Name() != "error" {// 排除第二个返回值不是 error 的continue}t := v.Type().Method(i) // 这里用的是reflect.Type.Method,因为要获取函数名字methodName := t.Nameif 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())
}

我们将路径分割得到一个切片,第一个元素为对象名(即mathstring),第二个元素为方法名(即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)}
}

运行:

$ 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"}

当然,这只是一个简单的实现,还有很多错误处理没有考虑,方法参数的类型目前只支持intstring,感兴趣可以去完善一下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/318866.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

目标检测-One Stage-RetinaNet

文章目录 前言一、RetinaNet的网络结构和流程二、RetinaNet的创新点Balanced Cross EntropyFocal Loss 总结 前言 根据前文目标检测-One Stage-YOLOv2可以看出YOLOv2的速度和精度都有相当程度的提升&#xff0c;但是One Stage目标检测模型仍存在一个很大的问题&#xff1a; 前…

【REST2SQL】03 GO读取JSON文件

REST2SQL需要一些配置信息&#xff0c;用JSON文件保存&#xff0c;比如config.json 1 创建config.json配置文件 {"hostPort":"localhost:5217","connString":"oracle://blma:5217127.0.0.1:1521/CQYH","_oracle":"ora…

【JUC】Synchronized及JVM底层原理

Synchronized使用方式 Synchronized有三种应用方式 作用于实例方法&#xff0c;当前示实例加锁进入同步代码前要获得当前实例的锁&#xff0c;即synchronized普通同步方法&#xff0c;调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。 如果设置了&#xff0c;执行…

pytest-yaml 测试平台-4.生成allure报告,报告反馈企业微信、钉钉、飞书通知

前言 定时任务执行完成后生成可视化allure报告&#xff0c;并把结果发到企业微信&#xff0c;钉钉&#xff0c;飞书通知群里。 生成allure报告 添加定时任务 执行完成后生成allure报告 查看报告详情 报告会显示详细的request 和 response 详细信息 也可以查看log日志 …

Navicat for Mysql怎么执行创建表的脚本

Navicat for Mysql怎么执行创建表的脚本 Navicat 怎么执行sql文件 Navicat 执行创建表语句 Navicat 执行sql语句 Navicat 怎么创建表语句 1、打开Navicat数据库管理工具&#xff1b; 2、点击菜单栏上的“工具”&#xff0c;选择“命令列界面”&#xff1b; 打开了命令列界面…

智能分析网关V4智慧港口码头可视化视频智能监管方案

一、需求背景 近年来&#xff0c;水利港口码头正在进行智能化建设&#xff0c;现场管理已经是重中之重。港口作为货物、集装箱堆放及中转机构&#xff0c;具有昼夜不歇、天气多变、环境恶劣等特性&#xff0c;安全保卫工作显得更加重要。港口码头的巡检现场如何高效、快捷地对…

学习Vue 01 欢迎来到Vue的世界

学习Vue 01 欢迎来到Vue的世界 概述 Initially released in 2014, Vue.js has experienced rapid adoption, especially in 2018. Vue is a popular framework within the developer community, thanks to its ease of use and flexibility. If you are looking for a great …

2020年认证杯SPSSPRO杯数学建模D题(第一阶段)让电脑桌面飞起来全过程文档及程序

2020年认证杯SPSSPRO杯数学建模 D题 让电脑桌面飞起来 原题再现&#xff1a; 对于一些必须每天使用电脑工作的白领来说&#xff0c;电脑桌面有着非常特殊的意义&#xff0c;通常一些频繁使用或者比较重要的图标会一直保留在桌面上&#xff0c;但是随着时间的推移&#xff0c;…

计算机创新协会冬令营——暴力枚举题目02

再次欢迎大家参加此次的冬令营&#xff0c;我们协会欢迎所有志同道合的同学们。话不多说&#xff0c;先来看看今天的题目吧。♪(^∇^*) 题目 力扣题号&#xff1a;2367. 算术三元组的数目 注&#xff1a;下述题目和示例均来自力扣 题目 给你一个下标从 0 开始、严格递增 的整…

Winform中使用Websocket4Net实现Websocket客户端并定时存储接收数据到SQLite中

场景 SpringBootVue整合WebSocket实现前后端消息推送&#xff1a; SpringBootVue整合WebSocket实现前后端消息推送_websocket vue3.0 springboot 往客户端推送-CSDN博客 上面实现ws推送数据流程后&#xff0c;需要在windows上使用ws客户端定时记录收到的数据到文件中&#x…

C/C++动态内存分配 malloc、new、vector(简单讲述)

路虽远&#xff0c;行则将至 事虽难&#xff0c;做则必成 今天来主要讲C中动态内存分配 其中会穿插一些C的内容以及两者的比较 如果对C语言中的动态内存分配还不够理解的同学 可以看看我之前的博客:C语言动态分配 在讲解C的动态内存分配之前 我们先讲一下C内存模型 &#xff1…

利用深度学习图像识别技术实现教室人数识别

引言 在现代教育环境中&#xff0c;高效管理和监控教室成为了一个重要议题。随着人工智能技术的迅猛发展&#xff0c;特别是深度学习和图像识别领域的突破&#xff0c;我们现在可以通过智能系统来自动识别教室内的人数&#xff0c;从而实现更加智能化的教室管理。 深度学习与图…