经历这么多失败也值了, 我终于把掌握 golang 反射了, 总结几点经验

简介: 经历这么多失败也值了, 我终于把掌握 golang 反射了, 总结几点经验

无数次失败, 我终于把掌握 golang 反射了, 总结几点经验

image.png

golang 反射很好用, 也有很多坑。

代码在: https://github.com/tangx-labs/golang-reflect-demo

Kind 和 Type

在 golang 的反射中, 有两个可以表示 类型 的关键字, KindType

定义覆盖范围

Kind 的定义覆盖范围必 Type 要大。 Kind 在定义上要 更抽象Type 要更具体。

可以简单理解为, 如果 Kind , 那么 Type 可能是 公交车 、 消防车

内置类型字面值

https://github.com/tangx-labs/golang-reflect-demo/blob/master/kind_type_test.go#L10

虽然 Kind 的定义比 Type 要大, 但是在 内置 类型的时候,它们两的字面值 可能 是一样的。 也可能不一样

// kind_type_test.go

// 打印 kind 和 type 的值
func kind_type_value(v interface{}) {
    rv := reflect.ValueOf(v)
    fmt.Println(rv.Kind(), rv.Type())
}

// kind 和 type 相同字面值
func Test_Kind_Type_Same(t *testing.T) {
    name := "tangxin"
    age := 18

    kind_type_value(name) // string string
    kind_type_value(age)  // int int

    kind_type_value(&name) // ptr *string
    kind_type_value(&age)  // ptr *int
}

自定义类型

如果是自定义类型, 那 KindType 的字面量必然不一样, 哪怕自定类型是内置类型的扩展。

// 根据内置类型 string 的自定义类型
type MyString string

// kind 和 type 不同
func Test_KindType_Different(t *testing.T) {
    p := Person{
        Name: "tagnxin",
        Age:  18,
    }
    kind_type_value(p)  // struct main.Person
    kind_type_value(&p) // ptr *main.Person

    s1 := MyString("tangxin")
    kind_type_value(s1)  // string main.MyString
    kind_type_value(&s1) // ptr *main.MyString
}

其实这些都没什么用。

golang 反射三定律

定律一: 接口类型对象 可以转换为 反射对象

https://github.com/tangx-labs/golang-reflect-demo/blob/master/rule1_test.go#L10

通过 reflect.TypeOf(v)reflect.ValueOf ,可以将 interface{} 转为为 反射对象。 其中 reflect.Type 表示 反射对象类型, reflect.Value 表示 反射对象的值

// 第一定律: 对象类型转指针类型
func Test_Rule1(t *testing.T) {
    p := &Person{
        Name: "zhangsan",
        Age:  18,
        Addr: struct {
            City string
        }{
            City: "chengdu",
        },
    }
    rule1(p)  // ptr *main.Person
    rule1(&p) // ptr **main.Person
}

func rule1(v interface{}) {
    rv := reflect.ValueOf(v)
    fmt.Println(rv.Kind(), rv.Type())
}

定律二: 反射对象 可以转换为 接口对象

https://github.com/tangx-labs/golang-reflect-demo/blob/master/rule2_test.go#L25

反射对象使用 rv.Interface() 可以被还原为 接口对象 。具体一个对象能否被还原, 可以通过 rv.CanInterface() 进行检查。

  1. 接口检查: rv.CanInterface() 判断是否可以被转换成 Interface 类型
  2. 类型转换: irv:=rv.Interface()反射类型 rv 转换为 interface 类型
  3. 类型断言: 常规操作了, v,ok:=irv.(type)
func rule2(rv reflect.Value) {
    // check
    if !rv.CanInterface() {
        fmt.Println("rv is not settable: ", rv.Type())
        return
    }

    // convert
    rv = DerefValue(rv)
    irv := rv.Interface()
    fmt.Println(irv)

    // type assert
    v, ok := irv.(Person)
    fmt.Println(v, ok) // {zhangsan 18 {chengdu}} true

}

定律三: 反射类型如果要修改值, 则反射类型必须为 settable

https://github.com/tangx-labs/golang-reflect-demo/blob/master/rule3_test.go#L32
  1. 修改行为检查: rv.CanSet() 判断是否能进行值修改
  2. 修改值: golang reflect 包提供了很多对应类型的修改, 结构统一为 rv.SetXXXX(value)

    1. SetString(s)
    2. SetInt(i)
    3. SetBool(b)
    4. https://pkg.go.dev/reflect#Value.SetBool
func rule3(rv reflect.Value) {
    if !rv.CanSet() {
        fmt.Println("rv is not settable", rv.Type())
        return
    }

    switch rv.Kind() {
    case reflect.String:
        rv.SetString("tangxin")
    case reflect.Int:
        rv.SetInt(333)
    default:
        fmt.Println("not support kind: ", rv.Kind())
    }
}

reflect.Type 和 reflect.Value

reflect.Typereflect.Value 是反射中基础的基础。

反射指针对象 类型 与 反射容器对象 类型

指针 在 golang 中是一个比较特别的对象, 万事万物, 都可以获取到指针。在反射对象中也不例外。

反射容器对象 这个名字是我自己取的, 就是为了区别于 反射指针对象 以便随后阐述。 其实在 golang 的 reflect.Kind 定义中, 指针容器 对象是平级的。

const (
    Invalid Kind = iota
    Bool
    Int
  // ...
  String
  // ...
    Interface
    Map
    Ptr  // 这里是重点, 指针可以只想任何容器类型,包括指针本身。
)

指向指针的指针对象

如果需要通过 *main.Person反射指针对象 p 需要获取真实对象类型 main.Person ,可以使用 p.Elem() 方法。 但是, 如果 p 不是 指针对象 将会发生 panic

因此, golang 提供了 reflect.Indirect(rv) 方法获取真实对象类型。当 p 为指针时, 返回 p.Elem(), 否则返回 p 本身。

func Indirect(v Value) Value {
    if v.Kind() != Ptr {
        return v
    }
    return v.Elem()
}

但这里本身也有一个问题, p 是一个 指向指针的指针 ,如果值使用一次 reflect.Indirect() 可能得到的依旧是一个指针对象。

概念有点绕, 但确实存在, 例如结果如下 **main.Person

// rule1_test.go
// 指向指针的指针对象
func Test_Rule1(t *testing.T) {
    p := &Person{
        Name: "zhangsan",
        Age:  18,
        Addr: struct {
            City string
        }{
            City: "chengdu",
        },
    }

    rv := reflect.ValueOf(&p)
    fmt.Println(rv.Kind(), rv.Type()) // ptr **main.Person
}

在如此情况下, 在单独使用 reflect.Indirect() 就不好用了。

因此可以适当改造一下, 这样就可以保证不论有多少层 指针, 都可以拿到真实的的 反射容器对象

// value.go
// DerefValue 返回最底层的反射容器对象
func DerefValue(rv reflect.Value) reflect.Value {
    for rv.Kind() == reflect.Ptr {
        rv = rv.Elem()
    }

    return rv
}

同理, reflect.Type 也有这种情况。

https://github.com/tangx-labs/golang-reflect-demo/blob/master/type.go#L5

https://github.com/tangx-labs/golang-reflect-demo/blob/master/value.go#L5

获取 reflect.Type

一个对象 v反射类型 有两种方式获取。

  • reflect.TypeOf
  • rv.Type()

这两者的结果是相同的。

结构体方法调用

https://github.com/tangx-labs/golang-reflect-demo/blob/master/method_call_test.go#L31

调用结构体的方法, 也是 golang 反射中一个重要的特点。

  1. 使用 mv:=rv.MethodByName(name) 返回一个
  2. 使用 mv.IsValid() 检查对象是否合法。
  3. 使用 mv.Call(...In) 调用方法。

需要额外注意的是:

  1. 方法的 接收者 是有 指针 (s *student)结构体 (s student) 之分的。
  2. 在反射对象中 指针接收者 的方法是不能被 结构体接收者 调用。

type student struct {
    Name string
    Age  int
}

func (stu *student) SetDefaults() {
    stu.Name = "tangxin"
    if stu.Age == 0 {
        stu.Age = 100
    }
}

// 没有传参数的方法
func (stu *student) Greeting() {
    fmt.Printf("hello %s, %d years old\n", stu.Name, stu.Age)
}

// 具有传参数的方法
func (stu *student) Aloha(name string) {
    fmt.Println("aloha,", name)
}

func Test_MethodCall(t *testing.T) {
    stu := student{
        Name: "wangwu",
    }

    // 注意
    // 方法对象的方法接收者, 可以是 **指针对象** 也可以是 **结构体对象**
    // 如果是指针对象的方法, **结构体对象** 是不能调用起方法的
    rv := reflect.ValueOf(stu)
    prv := reflect.ValueOf(&stu)

    stu.Greeting()
    methodCall(prv, "SetDefaults")
    methodCall(rv, "Greeting") // 结构体接收者, 找不到方法
    methodCall(prv, "Aloha", reflect.ValueOf("boss"))
}

// 对象方法调用
// rv 目标对象, method 方法名称, in 参数
func methodCall(rv reflect.Value, method string, in ...reflect.Value) {

    // 通过方法名称获取 反射的方法对象
    mv := rv.MethodByName(method)
    // check mv 是否存在
    if !mv.IsValid() {
        fmt.Printf("mv is zero value, method %s not found\n", method)
        return
    }

    // 调用
    // nil 这里代表参数
    mv.Call(in)
}
相关文章
|
6月前
|
Go
Golang反射---结构体的操作案例大全
Golang反射---结构体的操作案例大全
41 0
|
3天前
|
JSON 监控 安全
Golang深入浅出之-Go语言中的反射(reflect):原理与实战应用
【5月更文挑战第1天】Go语言的反射允许运行时检查和修改结构,主要通过`reflect`包的`Type`和`Value`实现。然而,滥用反射可能导致代码复杂和性能下降。要安全使用,应注意避免过度使用,始终进行类型检查,并尊重封装。反射的应用包括动态接口实现、JSON序列化和元编程。理解反射原理并谨慎使用是关键,应尽量保持代码静态类型。
25 2
|
3天前
|
JSON 编译器 Go
Golang深入浅出之-结构体标签(Tags):JSON序列化与反射应用
【4月更文挑战第22天】Go语言结构体标签用于添加元信息,常用于JSON序列化和ORM框架。本文聚焦JSON序列化和反射应用,讨论了如何使用`json`标签处理敏感字段、实现`omitempty`、自定义字段名和嵌套结构体。同时,通过反射访问标签信息,但应注意反射可能带来的性能问题。正确使用结构体标签能提升代码质量和安全性。
19 0
|
3天前
|
存储 Go
Golang底层原理剖析之反射reflect
Golang底层原理剖析之反射reflect
26 0
|
9月前
|
存储 Java Go
Golang的反射reflect深入理解和示例
Golang的反射reflect深入理解和示例
|
8月前
|
存储 Go
反射三定律带你初步了解Golang反射的概念~
反射三定律带你初步了解Golang反射的概念~
|
10月前
|
Go
|
12月前
|
Go
深入了解Golang中的反射机制
深入了解Golang中的反射机制
122 0
|
XML JSON Go
Golang 反射
Go语言是静态编译类语言,定义变量时就已经知道其是什么类型,但是我们在使用过程中有时候会遇到参数时interface{}类型的参数。那么,调用时就可以传递任何类型的参数,那么在函数内部想要知道传递的是什么类型的参数就需要使用反射。
78 0