公众号:程序员读书,欢迎关注
在Go语言的标准库中,有很多的包功能很强大,比如 fmt
包的函数可以把任何类型的数值输出到控制台,再比如 enconding/json
可以将结构体、数组、切片等不同的数据类型序列化为一个 JSON
字符串,这些包之所以如此强大,是因为使用了Go语言的反射机制。
反射是什么?反射能做什么?,在这篇文章中,我们来了解一下!
什么是反射
反射是编程语言的高级特性之一,运用反射机制可以在程序运行时动态检查变量的类型与具体的值,并且可以动态修改变量的值或者调用变量内在支持的方法。
为什么需要反射
上面对反射的描述可以让你觉得有点难以理解,我们都知道变量的类型在代码编译时就能确定的,比如下面的代码中变量 n
在编译时就确定为 int
:
go
package main
import (
"fmt"
)
func main() {
n := 10
fmt.Printf("%d %T", n, n)
}
既然编译时就已经知道变量的类型,为何还要去检测变量的类型呢?
这是因为有些时候我们要处理的类型是不确定的,比如 fmt
包下的输出函数允许输出任何类型数据,fmt
的输出函数大部分都接收类型为 interface{}
参数,这就表示只有在程序运行时才能确定要输出的值是什么类型。
我们自己来实现一个可以格式化任何数据的函数,该函数的参数同样是 interface{}
,表示可以格式化任何数据,我们可以用 switch
类型分支来判断数据类型:
go
type stringer interface {
String() string
}
func MyFormat(x interface{}) string {
switch x := x.(type) {
case stringer:
return x.String()
case string:
return x
case int:
return strconv.Itoa(x)
case bool:
if x {
return "true"
}
return "false"
case User: //自定义的User类型
return "My Name is:" + x.Name
default:
return "???"
}
}
在 MyFormat()
函数中,我们还判断了一个自定义的结构体 User
,接下来我们可调用该函数进行数据格式化:
go
package main
import (
"fmt"
"strconv"
)
type User struct {
Name string
Gender uint8
}
func main() {
fmt.Println(MyFormat(User{Name: "小明", Gender: 1}))
fmt.Println(MyFormat(12))
fmt.Println(MyFormat("str"))
}
但是,如果我们想格式化数组,切片,map
呢?你会发现 switch
类型分支无法判断数组、切片和 map
类型的。
而对于自定义类型,比如 User
,虽然可以判断,但却要一直添加 case
分支来判断,要知道自定义类型是无穷无尽且未知的。
这时候反射机制就派上用场了!
Go反射编程
当我们把一个值或变量赋给一个 interface{}
类型的变量时:
go
var i interface{}
n := 10
i = n
s := "test"
i = s
上面的代码中,空接口变量 i
由两个部分组成,当把 n
和 s
赋给 i
,由类型与值变化如下图所示:
反射的作用就在于检测空接口类型的类型(type
)和值(value
)是什么,Go语言的反射能力由 reflect
包提供,这个包里有两个重要的类型:Type
和 Value
,分别表示检测到的空接口现在的具体类型和值。
reflect.Type
Type
是一个接口,当我们把要检测的值传给 reflect.TypeOf()
函数后,会返回一个实现了 Type
接口实例,从 TypeOf()
函数的签名可以看出该函数的参数就是一个空接口类型:
go
func TypeOf(i interface{}) Type
Type
接口有许多的方法,比如我们可以调用 Type
的 Kind()
方法输出该接口当前的类型:
go
i := 10
t := reflect.TypeOf(i)
fmt.Println(t.Kind())//int
reflect.Value
Value
则代表变量的具体值,reflect
包的 ValueOf()
函数可以返回一个 Value
结构体,ValueOf()
函数的参数同样是一个 interface{}
类型:
go
func ValueOf(i interface{}) Value
Value
结构体同样有 Kind()
用于获取变量的类型:
go
i := 10
v := reflect.ValueOf(i)
fmt.Println(v.Kind()) //int
虽然类型可以无尽穷,但通过 Kind()
方法返回类型却是固定的,Kind()
返回以下面的取值范围:
go
package reflect
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
Ptr
Slice
String
Struct
UnsafePointer
)
实战
现在我们使用反射机制来完善我们的格式化函数 MyFormat
:
go
func MyFormat(x interface{}) string {
v := reflect.ValueOf(x)
switch v.Kind() {
case reflect.Invalid:
return "invalid"
case reflect.Array, reflect.Slice:
str := "["
for i := 0; i < v.Len(); i++ {
str += MyFormat(v.Index(i).Interface())
if i < v.Len()-1 {
str += ","
}
}
return str + "]"
case reflect.Struct:
str := "{"
for i := 0; i < v.NumField(); i++ {
str += MyFormat(v.Field(i).Interface())
if i < v.NumField()-1 {
str += ","
}
}
return str + "}"
case reflect.String:
return v.String()
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64:
return fmt.Sprintf("%d", v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return fmt.Sprintf("%d", v.Uint())
case reflect.Bool:
if v.Bool() {
return "true"
}
return "false"
case reflect.Map:
str := "{"
for i, key := range v.MapKeys() {
str += key.String() + ":" + MyFormat(v.MapIndex(key).Interface())
if i < v.Len()-1 {
str += ","
}
}
str += "}"
return str
default:
return "invalid"
}
}
现在调用 MyFormat()
函数,可以格式化结构体,Map
,数组,切片等类型,对于未知的类型,可以通过Kind()方法判断其底层类型,因为最终都可以格式化:
go
package main
import (
"fmt"
"reflect"
)
type Address struct {
Detail string
}
func main() {
user := User{
Name: "小明",
Gender: 1,
MyAddress: Address{Detail: "我的地址"},
Label: map[string]string{"爱好": "打球"},
}
fmt.Println(MyFormat(user))
fmt.Println(MyFormat([]int{1, 2, 3}))
fmt.Println(MyFormat(map[string]string{"11": "test"}))
}
type User struct {
Name string
Gender uint8
MyAddress Address
Label map[string]string
}
在 MyFormat()
中,我们可以看到对数组和切片、结构体、Map
这些复杂类型的处理,分别调用不同的方法,比如当类型为Map时可以调用 MapKeys()
方法返回Map的所有key,而是结构体,可以调用 NumField()
方法返回所有字段。
反射除了检测变量的值,还可能动态设置变量的值,接下来我们来实现一个将 HTTP Query
参数解析到结构体的功能。
ini
id=1&name=小明
我们要实现的功能是将上面的类似 HTTP Query
请求参数的字符串映射到一个结构体当中,首先实现一个将字符串 HTTP Query
字符串解析为 Map
的函数:
go
func MockQueryParams(queryParamStr string) map[string]string {
queryParamArr := strings.Split(queryParamStr, "&")
Params := make(map[string]string)
for _, v := range queryParamArr {
s := strings.Split(v, "=")
Params[s[0]] = s[1]
}
return Params
}
再接着,将解析得到的 Map
反射到结构体当中,这里要调用 reflect.Value
的 Elem()
方法来获得结构体的指针,而要修改结构体的字段的值,则就需要判断字段的类型,并调用相应的方法,比如字符串,就调用 Value
的 SetString()
方法:
go
func ParseHttpQuery(queryParams map[string]string, params interface{}) error {
v := reflect.ValueOf(params).Elem()
fields := make(map[string]reflect.Value)
//解析结构体
for i := 0; i < v.NumField(); i++ {
fieldInfo := v.Type().Field(i)
tag := fieldInfo.Tag
name := tag.Get("data")
if name == "" {
name = strings.ToLower(fieldInfo.Name)
}
fields[name] = v.Field(i)
}
//遍历参数
for name, value := range queryParams {
f := fields[name]
if !f.IsValid() {
continue
}
if f.Kind() == reflect.Slice {
elem := reflect.New(f.Type().Elem()).Elem()
if err := setValue(elem, value); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
f.Set(reflect.Append(f, elem))
} else {
if err := setValue(f, value); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
}
}
return nil
}
func setValue(v reflect.Value, value string) error {
switch v.Kind() {
case reflect.String:
v.SetString(value)
case reflect.Int:
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
v.SetInt(i)
case reflect.Bool:
b, err := strconv.ParseBool(value)
if err != nil {
return err
}
v.SetBool(b)
default:
return fmt.Errorf("unsupported kind %s", v.Type())
}
return nil
}
功能实现后就在可以实现数据解析了:
go
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
type User struct {
ID int `data:"id"`
Name string `data:"name"`
}
func main() {
//模拟HTTP Query参数
queryParamStr := "id=1&name=小明"
//将HTTP Query解析为Map
Params := MockQueryParams(queryParamStr)
u := &User{}
ParseHttpQuery(Params, u)
//输出:{1,"小明"}
fmt.Println(u)
}
使用反射的几点建议
尽量不要使用反射,主要原因有三点:
- 反射代码运行效率低
- 反射代码比较难懂
- 反射代码容易出错
小结
反射是一种元编程能力,可以动态检测和调用变量的方法等,对于要写出能兼容不同类型且能处理未知道数据类型的代码,反射机制非常有用,但是反射机制的代码往往让开发人员能难理解,因此应该慎重使用反射机制。