掘金 后端 ( ) • 2024-06-05 16:48

在了解了反射的基本概念、整体设计原理之后,接下来需要考虑的就是反射的具体用法,使用反射时首先需要考虑的是到底使用类型的反射,还是值的反射,整体而言,如果想获取的变量的信息和具体的值无关,仅仅是变量的类型包含的信息,或者说无论变量是什么值,获取的信息都是一致的,那么就使用类型反射,否则使用值的反射,本文将会详细地过一遍类型反射对象暴露的方法,后续再介绍值反射对象的方法。

1. Kind:

用于获取类型的种类,一系列相同的类型归为一个种类,如种类struct,自定义的结构体类型或标准库中的结构体类型,都会归为struct种类,种类的全部枚举可在 reflect库中查看:

type Kind uint

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Pointer
    Slice
    String
    Struct
    UnsafePointer
)

接下来是具体的代码示例:

type Demo struct {
    A string
    B int
}

func (d Demo) hello() {}

type I interface {
    hello()
}

func main() {
    var a Demo
    // 直接取a的类型反射
    aType := reflect.TypeOf(a)
    fmt.Println(aType.Kind())

    // 将a赋值给一个接口,之后取接口b的类型反射
    var b I = a
    bType := reflect.TypeOf(b)
    fmt.Println(bType.Kind())

    // 将接口b赋值给一个空接口,之后取空接口c的类型反射
    var c interface{} = b
    cType := reflect.TypeOf(c)
    fmt.Println(cType.Kind())
}

// output:
// struct
// struct
// struct

变量a反射的种类为struct,这是理所当然的,但是b和c的种类则会让人有一些疑惑,种类的枚举中是存在Interface的,那么将一个结构体赋值给一个接口之后再对 这个接口进行反射,那么变量的种类到底是接口还是结构体,从最终的输出来看,结果为struct,其实从原理思考这个问题,就不难得到答案,变量的类型信息是保存在接口中的,当接口赋值给另一个接口,就会将接口中保存的类型信息和值信息赋值给另一个接口变量的对应字段,最后赋值给TypeOf()的形参时,会再将这些类型信息和值信息赋值给一个空接口,再基于空接口中的类型信息和值信息进行反射。

所以对接口进行反射,最终获取到的是接口中保存的实际变量的类型,接下来可以看一些其他的情况:

type Demo struct {
    A string
    B int
}

func (d Demo) hello() {}

type I interface {
    hello()
}

func main() {
    var a Demo
    var b interface{} = a
    var c I = nil
    var d interface{} = c
    bType := reflect.TypeOf(b)
    fmt.Println(bType)
    dType := reflect.TypeOf(d)
    fmt.Println(dType)
}


// output:
// main.Demo
// <nil>

b中保存着变量a的类型信息,故最终的结果为a的类型: main.Demo,c是一个接口类型的nil值,将其赋值给一个空接口d,之后取d的类型反射,最终的结果为nil,因为将nil值赋值给一个接口时,nil值本身是不包含类型信息的,并不是某个具体类型的nil值,所以最终的类型信息还是nil,但如果是如下代码:

func main() {
    var c []string = nil
    var d interface{} = c
    dType := reflect.TypeOf(d)
    fmt.Println(dType)
}

// output:
// []string

此时展示类型信息的时候,则为[]string,不再是nil,这是因为c虽然是nil,但是是slice类型的nil,将其赋值给空接口d的时候,d是可以拿到c的类型信息的,故对d进行类型反射的时候,会拿到d中实际保存的变量c的类型信息。

在以上的例子中,打印一个接口的类型时,最终打印出的都是接口中包含的实际类型的信息,那么在何种情况下,Kind方法的结果会是Interface自身呢,以下是一些具体的情况:

func main() {
    s := make([]interface{}, 0)
    s = append(s, 1)
    sType := reflect.TypeOf(s)
    fmt.Println(sType.Kind())
    fmt.Println(sType.Elem().Kind())
    sValue := reflect.ValueOf(s)
    fmt.Println(sValue.Index(0).Elem().Kind())
}

// output:
// slice
// interface
// int

切片本身的反射的Kind为slice,使用Elem方法取切片的元素的类型反射对象,其Kind值为interface,这种情况下元素类型的种类就是interface,而不是接口中实际包含的int,这是因为元素的类型信息是保存在切片的类型信息中的,而不是直接赋值给另一个接口,故获取切片的元素的种类时获取到的就是interface, 如果想获取每一接口中实际的类型信息,需要先使用值反射,之后用Index(0)拿到第一个元素,再使用Elem()获取接口中实际的变量,之后再使用Kind()获取种类, 此时获取到的就是int。

2. Name, String

这两者均返回类型的描述信息,Name方法返回类型的名称,并不包含包名,String方法返回类型的描述信息,实现Stringer接口,用于fmt相关函数打印类型信息,这两者最主要的区别应该就是对于字面量类型(未用type定义名称的类型)的打印结果,Name方法对于字面量类型返回的结果为空字符串,String方法返回的内容 是字面量的描述信息,具体示例如下:

type Demo struct {
    a int
}

func main() {
    a := Demo{}
    aType := reflect.TypeOf(a)
    fmt.Println(aType.Name())
    fmt.Println(aType.String())

    b := struct {
       a int
    }{}
    bType := reflect.TypeOf(b)
    fmt.Println(bType.Name())
    fmt.Println(bType.String())
}

// output:
// Demo
// main.Demo
//
// struct { a int }

对于具名类型Demo的变量,Name以及String类型的结果分别为Demo以及main.Demo,String返回的内容包含了类型所在的包的包名,而变量b则是一个字面量类型的变量,这个类型是未使用type关键字定义名称的,故Name()方法返回的名称就是一个空字符串,但String方法则是返回的则是该类型的描述信息,即struct { a int }。

3. PkgPath

PkgPath将返回一个类型所在的包的路径,这个路径可以是标准库的路径也可以是三方库的路径,对于内建类型(如int,error)由于本身就不具备包名,故该方法返回值为空字符串,对于字面量类型返回值也是空字符串,以下为具体示例:

type Demo struct {
    a int
}

func main() {
    a := Demo{}
    aType := reflect.TypeOf(a)
    fmt.Printf("a: %v\n", aType.PkgPath())

    b := gin.Param{}
    bType := reflect.TypeOf(b)
    fmt.Printf("b: %v\n", bType.PkgPath())

    c := &gin.Param{}
    cType := reflect.TypeOf(c)
    fmt.Printf("c: %v\n", cType.PkgPath())
    fmt.Printf("c: %v\n", cType.String())

    d := 1
    dType := reflect.TypeOf(d)
    fmt.Printf("d: %v\n", dType.PkgPath())
    fmt.Printf("d: %v\n", dType.String())
}

// output:
// a: main
// b: github.com/gin-gonic/gin
// c:
// c: *gin.Param
// d:
// d: int

变量a的类型Demo是定义在main包中的,故其包路径返回值为main,b变量类型为gin包中的Param,故其包路径返回值为github.com/gin-gonic/gin,对于c,是对gin.Param的变量取了地址,故c是一个指针变量,但这个指针变量并未定义为一个新的具名类型,故其是一个字面量类型,所以包路径的返回结果为空,使用String可以看到具体的字面量信息为*gin.Param,最后d变量的类型为int,是一个内建类型,不存在包路径,故其包路径返回结果也为空。

4. Implements, ConvertibleTo, AssignableTo, Comparable

这四个方法的作用比较接近,都是判读类型是否存在某种性质,Implements方法可以判断类型是否实现了某个接口,AssignableTo可以判断一个变量是否可以赋值给另一个变量,ConvertibleTo可以判断一个变量是否可以强转为另一个变量,Comparable可以判断一个变量是否是可比较的。

Implements很好理解,就是判断一个类型是否实现了某个接口,当类型定义了接口规定的全部方法时,类型就自动实现了该接口,Implements方法的参数必须是一个接口类型的反射变量,如果是其他类型会导致该方法panic,以下是一些基本的例子:

type IDemo interface {
    hello()
}

type Demo struct {
    a int
}

func (d *Demo) hello() {

}

func main() {
    a := &Demo{}
    aType := reflect.TypeOf(a)
    // 因为需要获取接口类型的反射变量,所以这里使用接口的指针类型,之后再使用Elem
    iType := reflect.TypeOf((*IDemo)(nil)).Elem()
    fmt.Println(aType.Implements(iType))

    // interface{}不包含任何方法,故任何类型都会实现该接口
    iType = reflect.TypeOf((*interface{})(nil)).Elem()
    fmt.Println(aType.Implements(iType))
}

ConvertibleTo代表两个变量之间是否可以进行强制类型转换,类似于string和[]byte可以进行强制类型转换,以下是一些示例:

type IDemo interface {
    hello()
}

type IDemo2 interface {
    IDemo
    hello2()
}

type Demo struct {
    a int
}

func (d *Demo) hello() {

}

func main() {
    var str string
    var bytes []byte
    sType := reflect.TypeOf(str)
    bType := reflect.TypeOf(bytes)
    // 这里表示字符串类型的变量可以强转为[]byte类型
    fmt.Println(sType.ConvertibleTo(bType))

    var i int
    iType := reflect.TypeOf(i)
    // int类型不能转换为[]byte
    fmt.Println(iType.ConvertibleTo(bType))

    // 任何变量都可以强转为空接口类型的变量
    eType := reflect.TypeOf((*interface{})(nil)).Elem()
    fmt.Println(iType.ConvertibleTo(eType))

    iDType := reflect.TypeOf((*IDemo)(nil)).Elem()
    iD2Type := reflect.TypeOf((*IDemo2)(nil)).Elem()
    // IDemo2接口内嵌了IDemo接口,IDemo2接口需要实现的方法比IDemo多了一个,故IDemo2可以强转为IDemo,但是IDemo不能强转为IDemo2
    fmt.Println(iDType.ConvertibleTo(iD2Type))
    fmt.Println(iD2Type.ConvertibleTo(iDType))
}

AssignableTo表示一个变量是否可以赋值给另一个变量,比如同类型的两个变量肯定可以相互赋值,一个变量也可以赋值给自身实现了的接口的变量,如果两个类型的基础类型是相同的,并且并不全部是具名类型,则两者也是可以相互赋值的,具体例子如下:

type IDemo interface {
    hello()
}

type Demo struct {
    a int
}

func (d *Demo) hello() {

}

func main() {
    var a int
    var b int
    aType := reflect.TypeOf(a)
    bType := reflect.TypeOf(b)
    // a,b都是int类型,故a变量可以赋值给b变量
    fmt.Printf("a->b: %v\n", aType.AssignableTo(bType))

    // Demo类型实现了IDemo接口,但d变量无法赋值给IDemo类型的变量,
    // 因为Demo实现hello方法时是接收器是一个指针,故只能将*Demo类型的变量赋值给
    // IDemo类型的变量,所以这里d不能赋值给接口,但是e变量可以赋值
    var d Demo
    var e *Demo
    dType := reflect.TypeOf(d)
    eType := reflect.TypeOf(e)
    d2Type := reflect.TypeOf((*IDemo)(nil)).Elem()
    fmt.Printf("d->IDemo: %v\n", dType.AssignableTo(d2Type))
    fmt.Printf("e->IDemo: %v\n", eType.AssignableTo(d2Type))

    // x是直接基于字面量类型创建的变量,并且基础类型和Demo是一致的,故两者也是可以相互赋值的
    x := struct {
       a int
    }{1}
    xType := reflect.TypeOf(x)
    fmt.Printf("d->x: %v", dType.AssignableTo(xType))
}

// output:
// a->b: true
// d->IDemo: false
// e->IDemo: true
// d->x: true

可赋值性还有很多其他的规则,具体可以参考go语言规范: https://go.dev/ref/spec#Assignability

Comparable用于判断一个变量是否是可比较的,即是否可以用 > = 这种比较运算符进行比较,如果变量是可以进行比较的,就可以用于做map的key

5.Elem

方法的含义是获取变量元素的类型,这里的元素(Elem)在不同类型变量的含义上有所区别,类型只能是数组,管道,map,指针或者切片,其余类型会直接panic,对于数组、切片而言Elem得到的是数组元素的类型信息,对于map而言,Elem得到的是map中value的的类型信息,对于管道而言,Elem得到的是管道传递的变量类型信息,对于指针而言,Elem得到的是指针指向的变量的类型信息,所以一些指针类型变量反射之后往往会调用一下Elem获取指针指向的实际类型信息

func main() {
    // 数组的元素为int类型
    a := [2]int{1, 2}
    aType := reflect.TypeOf(a)
    fmt.Println(aType.Elem().Kind())
    
    // 切片的元素为float32类型
    b := []float32{1, 2}
    bType := reflect.TypeOf(b)
    fmt.Println(bType.Elem().Kind())
    
    // map的值为float64类型
    c := map[string]float64{"a": 1, "b": 2}
    cType := reflect.TypeOf(c)
    fmt.Println(cType.Elem().Kind())
    
    // 管道传递的变量为int32类型
    d := make(chan int32, 1)
    dType := reflect.TypeOf(d)
    fmt.Println(dType.Elem().Kind())
    
    // 指针所指向的变量为数组类型
    e := &a
    eType := reflect.TypeOf(e)
    fmt.Println(eType.Elem().Kind())
}

6.Field,FieldByIndex,FieldByName,FieldByNameFunc,NumField

这些方法都和结构体的字段有关,所以这些方法也只适用于结构体的类型反射变量,其他的类型反射对象调用这些方法会直接panic,以下是具体的使用示例:

type People struct {
    Name   string  `json:"name"`
    Age    int     `json:"age" yaml:"age" demo:"test"`
    Weight float64 `json:"weight"`
    Height float64 `json:"height"`
}

type Student struct {
    People
    Class string `json:"class"`
}

func main() {
    p := &People{
       Name:   "hayson",
       Age:    18,
       Weight: 70,
       Height: 180,
    }
    // 如果直接获取p变量的类型反射,调用NumField会直接panic,
    // 因为此时的反射对象是针对指针的类型反射对象,而不是结构体,
    // 需要Elem访问到指针指向的结构体,之后针对于结构体获取字段相关信息
    // pType := reflect.TypeOf(p)
    // fmt.Println(pType.NumField())
    pType := reflect.TypeOf(p).Elem()
    fmt.Println(pType.NumField())

    // 基于字段索引依次获取每个字段对象
    for i := 0; i < pType.NumField(); i++ {
       field := pType.Field(i)
       // 字段名称以及字段的类型
       fmt.Println(field.Name, " ", field.Type)
       // 获取字段的tag信息
       fmt.Println(field.Tag.Get("json"), " ", field.Tag.Get("demo"))
       fmt.Println()
    }
}

针对于包含内嵌其他结构体的类型,一定要记住不能将内嵌的结构体理解为继承,内嵌的结构体其实就是隐藏包含着一个和内嵌结构体类型名称相同的字段(组合的思想), 用反射可以非常直观地理解这项内容,以下为一个具体的例子:

type People struct {
    Name   string  `json:"name"`
    Age    int     `json:"age" yaml:"age" demo:"test"`
    Weight float64 `json:"weight"`
    Height float64 `json:"height"`
}

type Student struct {
    People
    Class string `json:"class"`
}

func main() {
    s := &Student{
       People: People{
          Name:   "hayson",
          Age:    18,
          Weight: 70,
          Height: 180,
       },
       Class: "6-2",
    }
    sType := reflect.TypeOf(s).Elem()
    // 结构体的字段数为2,而不是5
    fmt.Println(sType.NumField())
    // 依次遍历结构体的字段
    for i := 0; i < sType.NumField(); i++ {
       field := sType.Field(i)
       // 字段名称以及字段类型
       // 可以看到结构体的第一个字段名称为People,类型为main.People,
       // 这说明内嵌的结构体本质上是一个单独的字段,并且字段名为类型的名称
       fmt.Println(field.Name, " ", field.Type)
       // 可以通过Anonymous属性判断该是否为嵌入字段,
       // 可以看到People字段为嵌入字段,而Class字段为非嵌入字段
       fmt.Println(field.Anonymous)
    }
}

对于多级嵌套的结构体,取层级比较深的字段,如果一级级定位会非常麻烦,标准库提供了可以直接定位到多级字段的方法FieldByIndex

type People struct {
    Name   string  `json:"name"`
    Age    int     `json:"age" yaml:"age" demo:"test"`
    Weight float64 `json:"weight"`
    Height float64 `json:"height"`
}

type Student struct {
    People
    Class string `json:"class"`
}

func main() {
    s := &Student{
       People: People{
          Name:   "hayson",
          Age:    18,
          Weight: 70,
          Height: 180,
       },
       Class: "6-2",
    }
    sType := reflect.TypeOf(s).Elem()
    // 传入index为0,0时,表示取结构体第一个字段的第一个字段,故这里取出的字段为Name
    field := sType.FieldByIndex([]int{0, 0})
    fmt.Println(field.Name)
    // 可以通过Index属性,得到Age字段对应的索引值
    fmt.Println(field.Index)
}

7. NumIn, NumOut, In, Out, IsVariadic

这些方法都和函数的反射相关,用于其他类型变量的类型反射,将会导致panic,并且这些方法都是类型反射的对象,所以获取到的信息只有函数的类型信息而不包含函数的实体,也就是说获取到的是函数的签名信息,比如有哪些参数有哪些返回值,但是并不包含函数的实体,所以不能用类型反射对象实际调用函数,以下是一些具体的用法:

func demo(a, b int) int {
    return a + b
}

func demo2(a int, b ...int) {
    fmt.Println(a, b)
}

func main() {
    dType := reflect.TypeOf(demo)
    fmt.Printf("参数个数: %v, 返回值个数: %v\n", dType.NumIn(), dType.NumOut())
    // 依次获取每一个参数的类型信息
    for i := 0; i < dType.NumIn(); i++ {
       in := dType.In(i)
       fmt.Printf("参数类型: %v\n", in)
    }
    for i := 0; i < dType.NumOut(); i++ {
       out := dType.Out(i)
       fmt.Printf("返回值类型: %v\n", out)
    }

    // IsVariadic将返回该函数是不是可变参数的
    d2Type := reflect.TypeOf(demo2)
    // 可以看到demo是不包含可变参数的,demo2是包含可以变参数的
    fmt.Printf("demo是否包含可变参数: %v,demo2是否包含可变参数: %v\n", dType.IsVariadic(), d2Type.IsVariadic())
    // 对于一个可变参数的函数,如demo2,参数个数还是2,同时针对于可变参数b,
    // 通过反射获取到的参数类型为[]int,和在函数内直接使用b参数的效果是一致的
    fmt.Printf("demo2 参数个数: %v\n", d2Type.NumIn())
    fmt.Printf("demo2 可变参数的参数类型: %v\n", d2Type.In(1))
}

8. NumMethod, Method, MethodByName

这些都是结构体方法相关的,和操作结构体字段的方法类似,有一点需要注意的是通过反射获取到的字段信息或者方法信息时都只能获取到包导出的部分(大写开头),因为reflect包的代码也都在单独的一个包,如果可以获取另一个包的隐藏字段,就违背了golang关于封装性的设计,所以反射获取信息时都只能获取到已导出的信息。

整体而言,方法在go中其实就是函数,只是这个函数的第一个参数都是接收者,接收者又通过是不是指针分为指针接收者和值接收者,之所以要做这样的区分,是因为接收者本质上是一个参数,当接收者为值类型时,方法内部操作的结构体的变量其实是一份副本,但如果是指针接收者的话方法内部可以通过指针获取到结构体自身,从而改变其属性,这和结构体直接作为函数参数的道理是完全一样的。

出于这样的原因,一个结构体的指针拥有的方法和一个结构体拥有的方法是不同的,结构体指针拥有指针接收者方法和值接收者方法,但是结构体只拥有值接收者的方法,因为指针接收者隐藏的含义是有可能在方法内部通过指针改变结构体的字段值,用一个结构体调用指针接收者的方法时这个结构体有可能是一个副本,方法内部的指针也是副本的指针,这是绝对不满足预期的,故指针接收者的方法只能通过结构体的指针来调用,以下是一段示例:

type Demo struct{}

func (d *Demo) Add(a, b int) int {
    return a + b
}

func (d *Demo) mul(a, b int) int {
    return a * b
}

func (d Demo) Sub(a, b int) int {
    return a - b
}

func main() {
    // 以指针的方式获取结构体的方法信息
    dType := reflect.TypeOf(new(Demo))
    fmt.Printf("结构体指针方法个数: %v\n", dType.NumMethod())
    for i := 0; i < dType.NumMethod(); i++ {
       method := dType.Method(i)
       fmt.Println(method.Name)
    }
    fmt.Println()
    // 以结构体自身的方式获取方法信息
    dType = dType.Elem()
    fmt.Printf("结构体方法个数: %v\n", dType.NumMethod())
    for i := 0; i < dType.NumMethod(); i++ {
       method := dType.Method(i)
       fmt.Println(method.Name)
    }
}

输出:
结构体指针方法个数: 2
Add
Sub

结构体方法个数: 1
Sub

对于mul方法,因为并未导出,使用反射是感知不到它的存在的,之后用指针来获取结构体的方法时,方法数量为2,分别为Add以及Sub,但用结构体自身来获取方法时,数量为1,名称为Sub,这说明结构体指针同时持有指针接收者和值接收者的方法,但是结构体自身只拥有值接收者的方法。

接口也同样可以获取其方法的信息,只是方法的参数中不包含接收者,并且获取接口中规定的方法时也不会判断是不是导出的,因为我们在实现接口时也需要将未导出的方法一起实现,所以通过反射获取接口规定的方法时也必然包含未导出的,以下是具体的示例:

type Iface interface {
    Add(float64, int) float64

    Sub(float64, int) float64

    mul(float64, int) float64
}

func main() {
    var i Iface = nil
    iType := reflect.TypeOf(&i).Elem()
    fmt.Printf("接口方法个数: %v\n", iType.NumMethod())
    for j := 0; j < iType.NumMethod(); j++ {
       method := iType.Method(j)
       fmt.Println(method.Name)
    }

    // add方法参数信息
    methodAdd := iType.Method(0).Type
    fmt.Printf("add方法参数: %v\n", methodAdd.NumIn())
    for j := 0; j < methodAdd.NumIn(); j++ {
       in := methodAdd.In(j)
       fmt.Println(in)
    }
}

9. Key

这个方法的功能很简单,返回一个map的key的类型信息,因为只有map存在key的概念,所以其他类型的反射对象调用该方法时都将导致panic,以下为具体示例:

func main() {
    a := make(map[int]int)
    aType := reflect.TypeOf(a)
    fmt.Println(aType.Key())
}

10. Len

该方法将返回一个数组的长度,仅针对数组有效,其他类型调用该方法都会panic,那为什么切片和map也不能调用呢?这两个按说也有length的概念,其实主要是因为目前所有的方法都是针对于类型反射对象而言的,也就是说类型本身就包含长度时才能使用Len方法,切片和map具体的值不同,长度也是不同的,类型本身是不包含长度的概念的。需要在类型定义时就指定长度的其实只有数组,故该方法仅对数组有效,以下为具体示例:

func main() {
    a := [10]int{}
    aType := reflect.TypeOf(a)
    fmt.Println(aType.Len())
}

11. ChanDir

该方法将返回一个管道的方向,如果是其他类型调用该方法将导致panic,以下为具体示例:

func main() {
    a := make(chan int)
    b := make(chan<- int)
    c := make(<-chan int)
    aType, bType, cType := reflect.TypeOf(a), reflect.TypeOf(b), reflect.TypeOf(c)
    fmt.Println(aType.ChanDir(), bType.ChanDir(), cType.ChanDir())
}

12. Size

该方法将返回一个类型所占的大小,只计算这个类型本身占用的空间,不会计算类型中引用的数据所占的空间,本身也是类型的反射,肯定是不会考虑变量具体的值的相关信息的,以下是一个具体的例子:

func main() {
    a := make([]int, 3)
    aType := reflect.TypeOf(a)
    fmt.Println(aType.Size())

    b := make([]int, 30)
    bType := reflect.TypeOf(b)
    fmt.Println(bType.Size())
}

无论切片的长度是3还是30,Size的返回值都会是24,因为切片自身其实仅仅是一个切片头,结构如下:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

包含一个地址,一个长度,以及一个容量,64位机下均为8字节,故类型的大小为24字节。 另外还有一点就是这个大小是需要考虑内存对齐的,所以这个长度可能会超过结构体各个字段的大小总和。

以上就是类型反射对象暴露出的主要方法,后续将会介绍reflect.Value对象暴露出的主要方法。