Post

Go Reflect in Action

Go Reflect in Action

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
This post is licensed under CC BY 4.0 by the author.
Share