在开始介绍Go的反射机制前,需要发起两个灵魂发问。
反射是什么?
反射就是程序能够在运行时检查变量和值,求出它们的类型。
为什么需要反射?
我们经常会把函数的参数定义为空interface类型(比如下面这段代码),编译时我们并不知道传入test_type的是什么类型的数据,传入的数据的具体类型是运行期确定的。因为任何类型都可以声称自己实现了interface{},因此我们在函数传参时可以往该函数的参数里塞各种类型的数据。如果我们无法获取传入的interface的真实类型,那我们也就没法针对数据做出我们的逻辑处理了。
package main
// main.go
import (
"fmt"
)
func test_type(v interface {}) {
fmt.Println("value:", v)
}
func main() {
var x float64 = 9.0
var y int64 = 999
test_type(x)
test_type(y)
}
下面开始介绍Go的反射机制和特性。
1. 反射可以将interface类型变量转换为反射对象
如果我们希望知道传入的interface的具体类型,再根据其真实类型来做不同的处理,这里Go是怎么提供支持的呢?这里就会用到Go的反射机制,即使用reflect包来提供获取传入的v的实际数据类型和数值。
比如下面这个例子,我们首先使用reflect.TypeOf
获取inteface的具体类型,再使用t.Kind()来获取真实的数据类型,最后根据k的值做不同的逻辑处理。因此,我们在实现一个供外界调用的函数接口时,我们可以把函数参数定义为空interface,即允许调用方传入任何类型的数据,我们再利用反射机制确定运行期传入的真实的数据类型,然后再根据类型做不同的逻辑处理。
package main
// main.go
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func test_type(v interface {}) {
t := reflect.TypeOf(v)
s := reflect.ValueOf(v)
k := t.Kind()
fmt.Println("raw data:", v)
fmt.Println("type:", t)
fmt.Println("value:", s)
fmt.Println("kind:", k)
switch k {
case reflect.Int64:
fmt.Println("type is int64");
case reflect.Float64:
fmt.Println("type is float64");
default:
fmt.Println("unknown type");
}
fmt.Println("===================")
}
func main() {
var x float64 = 9.2
var y int64 = 999
m := map[int]string {1:"james", 2:"ken"}
u := User{"KK", 1}
test_type(x)
test_type(y)
test_type(m)
test_type(u)
}
输出
raw data: 9.2
type: float64
value: 9.2
kind: float64
type is float64
===================
raw data: 999
type: int64
value: 999
kind: int64
type is int64
===================
raw data: map[1:james 2:ken]
type: map[int]string
value: map[1:james 2:ken]
kind: map
unknown type
===================
raw data: {KK 1}
type: main.User
value: {KK 1}
kind: struct
unknown type
===================
interface转反射对象的应用场景很多,一个著名的场景就是HTTP服务器解析HTTP请求,因为请求的类型千奇百怪,我们把请求解析函数的参数定义为interface{},也就是支持任意数据类型的传入,再使用反射的特性获取传入数据的具体类型,根据类型做不同的处理。Gin的请求解析代码如下,同样也是使用了反射特性解析请求,可以参考一下。
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if obj == nil {
return nil
}
value := reflect.ValueOf(obj)
switch value.Kind() {
case reflect.Ptr:
return v.ValidateStruct(value.Elem().Interface())
case reflect.Struct:
return v.validateStruct(obj)
case reflect.Slice, reflect.Array:
count := value.Len()
validateRet := make(sliceValidateError, 0)
for i := 0; i < count; i++ {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err)
}
}
if len(validateRet) == 0 {
return nil
}
return validateRet
default:
return nil
}
}
2. 将对象转化为interface对象
反射的第二个能力,能够将从一个反射对象还原为原来的interface对象。下面的例子中,x,y,u,m都先转化为反射对象,然后再转化为interface。
package main
// main.go
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func main() {
var x,y,u,m interface{}
x = 9.2
y = 999
u = User{"KK",20}
m = map[int]string{1:"james"}
x1 := reflect.ValueOf(x)
y1 := reflect.ValueOf(y)
u1 := reflect.ValueOf(u)
m1 := reflect.ValueOf(m)
x2 := x1.Interface()
y2 := y1.Interface()
u2 := u1.Interface()
m2 := m1.Interface()
if reflect.DeepEqual(x, x2) && reflect.DeepEqual(y, y2) && reflect.DeepEqual(u, u2) &&reflect.DeepEqual(m, m2) {
fmt.Println("all equal!!!")
}
fmt.Println("x2=", x2)
fmt.Println("y2=", y2)
fmt.Println("u2=", u2)
fmt.Println("m2=", m2)
}
输出
all equal!!!
x2= 9.2
y2= 999
u2= {KK 20}
m2= map[1:james]
3. 设置反射对象的值
上面的两个例子都只用到反射对象的读操作,如果我们需要对反射对象进行值修改,应该怎么操作呢?
我们可以使用反射对象的SetFloat等方法来设置,注意传入函数的参数是指针类型,这样才能支持函数内修改数据,如果传入的是值类型但又直接在test_type里修改,就会引发panic。所以一定要注意,通过反射可以修改interface的值,但前提是必须获得interface的变量地址。
package main
// main.go
import (
"fmt"
"reflect"
)
func test_type(v interface {}) {
s := reflect.ValueOf(v)
t := reflect.TypeOf(v)
s.Elem().SetFloat(7.1)
fmt.Println("type=", t)
fmt.Println("kind=", t.Kind())
}
func main() {
var x float64 = 9.2
test_type(&x)
fmt.Println("x=", x)
}
输出
type= *float64
kind= ptr
x= 7.1
4. 利用反射特性读写复杂结构体
上面读写反射对象的场景都过于简单,然后我们日常面对的数据往往是复杂的结构体,里面内嵌着map,slice,struct等各种复杂类型的数据,因此我们需要掌握如何对复杂结构体进行读写。
下面这个例子,准备对这个User结构体进行反射处理,因为这个结构体内定义了map,array,struct等类型,比较符合我们日常处理的结构体的复杂度。
type Address struct {
City string
}
type User struct {
Name string
Age int
Score map[string]int
Friends *[5]string
Addr *Address
}
这个例子定义了record函数,利用反射特性,处理传入的各种类型,处理内容就是对数据进行读和写。
package main
// main.go
import (
"fmt"
"reflect"
)
type Address struct {
City string
}
type User struct {
Name string
Age int
Score map[string]int
Friends *[5]string
Addr *Address
}
func record(u interface{}) {
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
fmt.Println("record t", t)
fmt.Println("record v", v)
user := v.Elem()
typeOfT := user.Type()
//read
for i := 0; i < user.NumField(); i++ {
f := user.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
//可以直接根据结构体的字段名来读数据
name := user.FieldByName("Name")
fmt.Println(name.String())
//write
//写基础类型
user.FieldByName("Age").SetInt(100)
//写map
sc := user.FieldByName("Score").Interface()
fmt.Println("Score:",sc)
md, _ := sc.(map[string]int)
md["Math"] = 100
md["PE"] = 60
// 写array
friends := user.FieldByName("Friends").Interface().(*[5]string)
fmt.Println("friends:",friends)
friends[2] = "Sam"
friends[3] = "Tody"
// 写内嵌结构体
addr := user.FieldByName("Addr").Interface().(*Address)
fmt.Println("addr:",addr)
addr.City = "Zhuhai"
}
func main() {
u := User{"KK", 20, map[string]int{"Math":70}, &[5]string{"Tom", "Ken"}, &Address{"Guangzhou"}}
record(&u)
fmt.Println("record done, u=", u)
fmt.Println("record done, u.Friends", u.Friends)
fmt.Println("record done, u.Score", u.Score)
fmt.Println("record done, u.Addr", u.Addr)
}
输出
record t *main.User
record v &{KK 20 map[Math:70] 0xc0000b8000 0xc00008e1e0}
0: Name string = KK
1: Age int = 20
2: Score map[string]int = map[Math:70]
3: Friends *[5]string = &[Tom Ken ]
4: Addr *main.Address = &{Guangzhou}
KK
Score: map[Math:70]
friends: &[Tom Ken ]
addr: &{Guangzhou}
record done, u= {KK 100 map[Math:100 PE:60] 0xc0000b8000 0xc00008e1e0}
record done, u.Friends &[Tom Ken Sam Tody ]
record done, u.Score map[Math:100 PE:60]
record done, u.Addr &{Zhuhai}
5. 利用反射特性做函数回调
结构体不仅可以定义数据,还可以定义方法,我们可以利用反射特性实现函数调用,比如这里我们实现一个回调函数,当record被调用完成后,会执行传入的callback函数,实现函数回调。
这里注意三点:
- MethodByName方法可以让我们根据一个方法名获取一个方法对象,然后我们构建好该方法需要的参数,最后调用Call就达到了动态调用方法的目的。
- 获取到的方法我们可以使用IsValid 来判断是否可用(存在)。
- 的参数是一个Value类型的数组,所以需要的参数,我们必须要通过ValueOf函数进行转换。
package main
// main.go
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
Score int
}
func (u *User) Callback(name string, score int) {
//do something
fmt.Printf("Callback !!! name=%s, score=%d\n", name, score)
}
func record(u interface{}) {
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
fmt.Println("record t", t)
fmt.Println("record v", v)
user := v.Elem()
typeOfT := user.Type()
//do something
for i := 0; i < user.NumField(); i++ {
f := user.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
user.Field(2).SetInt(100)
fCallback := v.MethodByName("Callback")
if fCallback.IsValid() {
args := []reflect.Value{reflect.ValueOf(user.Field(0).Interface()), reflect.ValueOf(user.Field(2).Interface())}
fCallback.Call(args)
}
}
func main() {
u := User{"KK",20, 0}
record(&u)
fmt.Println("record done, u=", u)
}
输出
record t *main.User
record v &{KK 20 0}
0: Name string = KK
1: Age int = 20
2: Score int = 0
Callback !!! name=KK, score=100
record done, u= {KK 20 100}
6.反射值得使用吗
赞成派:
- 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。(HTTP请求解析,ORM)
- 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。(函数回调,异步通知)
反对派:
- 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。抽象的代码总是不好理解的,虽然代码很简洁。因为传入的参数类型是未知的,因此函数维护者很难能提前预测到数据风险,进而难以做出更为合适的防御性编程。
- Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。比如读写复杂结构体的例子中,万一
user.FieldByName("Name")
这里的字段名称写错为”name”,可不会编译不过,而是运行时才给你抛panic,让你程序直接挂掉。 - 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。