Reflect

反射是指计算机程序在运行时(Runtime)可以访问,检测和修改它本身状态或行为的一种能力。

程序编译后,变量被转换为内存地址,而变量名无法被编译器写入可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言,可以在编译器将变量的反射信息如字段名称、类型信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样可以在程序运行期获取类型的反射信息,并修改他们

静态语言:

  • C/C++ 不支持反射
  • Go,Java,C# 支持反射

动态语言:

  • Lua,JavaScript,可以在运行期访问程序自身的值与类型,故不需要反射特性

Go 提供了一种在运行时更新和检查变量的值、调用变量的方法的机制,但在编译器不知道这些变量的具体类型,这种机制被称为反射。Go 使用 reflect 包访问程序的反射信息。

实现原理

反射的基础 interface{}

Go 的接口是由两部分组成的,一部分是类型信息,另一部分是数据信息

var a = 1
var b interface{} = a

b的类型信息是int,数据信息是1,这两部分信息都是存储 b里面。

b的类型实际上是eface,它是一个空接口,在src/runtime/runtime2.go中,它的定义如下:

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

也就是说,一个 interface{} 中实际上既包含了变量的类型信息,也包含了类型的数据。 正因为如此,才可以通过反射来获取到变量的类型信息,以及变量的数据信息。

反射对象 reflect.Type & reflect.Value

反射,可以将接口类型变量转换为反射类型对象

  • reflect.TypeOf: 返回反射类型(returns the reflection Type that represents the dynamic type of i)
  • reflect.ValueOf: 返回反射值(returns a new Value initialized to the concrete value)
var a = 1
t := reflect.TypeOf(a)  // t = int

var b = "hello"
v := reflect.ValueOf(b)  // v = "hello"

看一下 TypeOfValueOf 的源码会发现,这两个方法都接收一个 interface{} 类型的参数,然后返回一个 reflect.Typereflect.Value 类型的值。这也就是为什么可以通过 reflect.TypeOfreflect.ValueOf 来获取到一个变量的类型和值的原因。

reflect.TypeOf() 源码:

func TypeOf(i any) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

reflect.ValueOf() 源码:

func ValueOf(i any) Value {
    if i == nil {
        return Value{}
    }

    // TODO: Maybe allow contents of a Value to live on the stack.
    // For now we make the contents always escape to the heap. It
    // makes life easier in a few places (see chanrecv/mapassign
    // comment below).
    escapes(i)

    return unpackEface(i)
}

// Dummy annotation marking that the value x escapes,
// for use in cases where the reflect code is so clever that
// the compiler cannot follow.
func escapes(x any) {
    if dummy.b {
        dummy.x = x
    }
}

var dummy struct {
    b bool
    x any
}

反射定律

在 Go 官方博客中关于反射的文章 laws-of-reflection 中,提到了三条反射定律:

  1. 反射可以将 interface 类型变量转换成反射对象。
  2. 反射可以将反射对象还原成 interface 对象。
  3. 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。

关于第 2 点:

可以通过 reflect.Value.Interface 来获取到反射对象的 interface 对象,也就是传递给 reflect.ValueOf 的那个变量本身。不过返回值类型是 interface{},所以需要进行类型断言。

type Student struct {
    Name string `json:"name1" db:"name2"`
    Age  int    `json:"age1" db:"age2"`
}

func main() {
    var s Student
    v := reflect.ValueOf(&s)

    // 将反射对象还原成interface对象
    i := v.Interface()
    fmt.Println(i.(*Student))
}

关于第 3 点:

可以通过 reflect.Value.CanSet 来判断一个反射对象是否是可设置的。如果是可设置的,就可以通过 reflect.Value.Set 来修改反射对象的值。

func main() {
    s := &Student{
        Name: "zhangSan",
        Age:  18,
    }
    v := reflect.ValueOf(s)

    fmt.Println("set ability of v:", v.CanSet())           // false
    fmt.Println("set ability of Elem:", v.Elem().CanSet()) // true
}

可设置要求:

  • 反射对象是一个指针
  • 这个指针指向的是一个可设置的变量

原因:

如果这个值只是一个普通的变量,这个值实际上被拷贝了一份。如果通过反射修改这个值,那么实际上是修改的这个拷贝的值,而不是原来的值。 所以 go 语言在这里做了一个限制。

为什么v.CanSet() == false ?

v 是一个指针,而 v.Elem() 是指针指向的值,对于这个指针本身,修改它是没有意义的,可以设想一下,如果修改了指针变量(也就是修改了指针变量指向的地址),那会发生什么呢?那样指针变量就不是指向 x 了, 而是指向了其他的变量,这样就不符合预期了。所以 v.CanSet() 返回的是 false

package demo

import (
	"fmt"
	"reflect"
	"testing"
)

type Student3 struct {
	Name string `json:"name1" db:"name2"`
	Age  int    `json:"age1" db:"age2"`
}

func Test3(t *testing.T) {
	s := &Student3{
		Name: "zhangSan",
		Age:  18,
	}
	v := reflect.ValueOf(s)

	fmt.Println("set ability of v:", v.CanSet())           // false
	fmt.Println("set ability of Elem:", v.Elem().CanSet()) // true

	if v.Elem().CanSet() {
		for i := 0; i < v.Elem().NumField(); i++ {
			switch v.Elem().Field(i).Kind() {
			case reflect.String:
				v.Elem().Field(i).Set(reflect.ValueOf("lisi"))
			case reflect.Int:
				v.Elem().Field(i).Set(reflect.ValueOf(20))
			}
		}
	}

	fmt.Println("v: ", v)
	fmt.Println("student: ", v.Interface().(*Student3))
}
$go test -v demo3_test.go
=== RUN   Test3
set ability of v: false
set ability of Elem: true
v:  &{lisi 20}
student:  &{lisi 20}
--- PASS: Test3 (0.00s)
PASS
ok      command-line-arguments  0.002s

测试代码

package demo1

import (
	"fmt"
	"reflect"
	"testing"
)

type student struct {
	name  string
	age   uint8
	infos interface{}
}

func TestReflect(t *testing.T) {
	s := &student{
		name: "zhangSan",
		age:  18,
		infos: map[string]interface{}{
			"class": "class1",
			"grade": uint8(1),
			"read": func(str string) {
				fmt.Println(str)
			},
		},
	}
	options := s.infos
	fmt.Println("infos type:", reflect.TypeOf(options))
	fmt.Println("infos value:", reflect.ValueOf(options))

	fmt.Println("infos.class type:", reflect.TypeOf(options.(map[string]interface{})["class"]))
	fmt.Println("infos.class value:", reflect.ValueOf(options.(map[string]interface{})["class"]))

	fmt.Println("infos.grade type:", reflect.TypeOf(options.(map[string]interface{})["grade"]))
	fmt.Println("infos.grade value:", reflect.ValueOf(options.(map[string]interface{})["grade"]))

	fmt.Println("infos.read type:", reflect.TypeOf(options.(map[string]interface{})["read"]))
	fmt.Println("infos.read value:", reflect.ValueOf(options.(map[string]interface{})["read"]))

	read := options.(map[string]interface{})["read"]
	if reflect.TypeOf(read).Kind() == reflect.Func {
		read.(func(str string))("I am reading!")
	}
}

测试:

$go mod init github.com/gerryyang/goinaction/src/reflect
$go mod tidy
$go test -v demo1_test.go
=== RUN   Test1
infos type: map[string]interface {}
infos value: map[class:class1 grade:1 read:0x4f8e00]
infos.class type: string
infos.class value: class1
infos.grade type: uint8
infos.grade value: 1
infos.read type: func(string)
infos.read value: 0x4f8e00
I am reading!
--- PASS: Test1 (0.00s)
PASS
ok      command-line-arguments  0.003s
package demo

import (
	"fmt"
	"reflect"
	"testing"
)

type Student struct {
	Name string `json:"name1" db:"name2"`
	Age  int    `json:"age1" db:"age2"`
}

func Test2(t *testing.T) {
	var s Student
	v := reflect.ValueOf(&s)

	// 类型
	ty := v.Type()

	// 获取字段
	for i := 0; i < ty.Elem().NumField(); i++ {
		f := ty.Elem().Field(i)
		fmt.Println(f.Tag.Get("json"))
		fmt.Println(f.Tag.Get("db"))
	}
}
$go test -v demo2_test.go
=== RUN   Test2
name1
name2
age1
age2
--- PASS: Test2 (0.00s)
PASS
ok      command-line-arguments  0.003s