Go基础:反射介绍及类型的判断
为什么要使用反射
我们知道fmt.Printf()函数能够输出任意类型的任意值,甚至是用户自定义的类型。假设我们现在没有学习反射,我们尝试用已有的知识来编写一个类似的函数。为了简化要求,我们实现的函数仅接受一个参数,返回一个字符串。我们可以用switch方法来根据不同的类型进行不同类型的字符串策略的选择:
func Sprint(x interface{}) string {
type stringer interface {
String() string
}
switch x := x.(type) {
case stringer:
return x.String()
case string:
return x
case int: // 对int16,uint等类型做同样的处理
return strconv.Itoa(x)
case bool:
if x {return "true"}
return "false"
default: // array、chan、func、map、pointer、slice、struct
return "???"
}
}
简单的写了几个之后,我们发现,我们需要对每一种类型都做一个详细实现。更可怕的是,对于[]float64,map[string]string等其他的类型,甚至有无限种,我们总不能添加无限多的分支吧。更何况还有自己命名的类型。
当我们无法了解一个未知类型的布局时,这段代码就无法继续实现它的功能了,这时,我们就需要反射了。
反射介绍
反射(reflection)是在 Java出现后迅速流行起来的一种概念,通过反射可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。
Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。
反射也可以让我们将类型本身作为第一类的值类型处理。
Go语言中的反射是由reflect包提供支持的,它定义了两个重要的类型Type和Value任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,并且reflect包提供了 reflect.TypeOf()和reflect.ValueOf()两个函数来获取任意对象的Value和Type。
类型对象
使用reflect.TypeOf()函数可以获得任意值的类型对象reflect.Type,程序通过类型对象可以访问任意值的类型信息:
var a int
t := reflect.TypeOf(a)
fmt.Println(t.Name(), t.Kind(), t.String())
// 输出
// int int int
- 定义了一个
int类型的变量 - 通过
reflect.TypeOf()取得变量a的类型对象t,t的类型为reflect.Type - 通过
reflect.Type的从成员函数,分别获取了类型名,种类和字符串。
reflect.Typeof()总是返回一个具体类型,而不是接口类型。
额外的,reflect.Type类型满足fmt.Stringer接口。因为输出一个接口值的动态类型在调试和日志中很常用,所以fmt.Printf()提供了一个简写方式%T,内部实现就使用了reflect.TypeOf():
fmt.Printf("%T\n", 3) // int
值对象
reflect.ValueOf()函数接受任意的interface{},并返回一个可以包含任何类型的值对象reflect.Value。
与reflect.TypeOf()类似,reflect.Valueof的返回值也都是具体值,不过reflect.Value类型也可以包含一个接口值。
v := reflect.ValueOf(3)
fmt.Printf("%v ", v)
fmt.Println(v.String())
// 输出
3 <int Value>
另一个与reflect.Value类似的是,reflect.Value类型也满足fmt.Stringer接口,但除非Value包含的是一个字符串,否则String方法的结果仅仅输出类型。通常,你需要使用fmt包的%v占位符,它会对reflect.Value进行特殊的处理。
调用Value对象的Type()方法会把它的类型以reflect.Type方式返回:
v := reflect.ValueOf(3) // 获取Value对象
t := v.Type() // 获取Type对象
fmt.Println(t.String()) // int
种类对象
在看完了Type对象和Value对象之后,好像对我们解决之前的问题并没有什么帮助,不用着急,以Type和Value为基础,接下来介绍的种类对象(Kind)将大展身手。
我们需要区分一个对象大的种类的时候,就会用种类对象(Kind)。
类型(Type)指的是系统原生数据类型,如int、string、bool、float32等类型,以及使用type关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。
通过使用reflect.Type的成员方法Kind(),便可获得一个reflect.Kind类型的常量对象。
type empty struct {} // 定义了一个结构体,属于自定义类型
t := reflect.TypeOf(empty{}) // 获取结构体的反射类型
fmt.Println("name :",t.Name()) // 输出:name : empty
fmt.Println("kind :", t.Kind()) // 输出:kind : stuck
所有的Kind定义,都可以在reflect包下的type.go的文件中找到:
// Kind表示类型所表示的特定类型。
// 零类型不是有效类型。
type Kind uint
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
)
可以看出:Kind的分类只有少数的几种:基础类型Bool、String以及各种数字类型;聚合类型Array和Struct;引用类型Chan、Func、Ptr、Slice和Map、接口类型Interface;最后还有Invalid类型,表示它们还没有任何值。
反射的使用
学会了利用反射来判断变量所属的大类之后,我们可以重写开头的函数:
利用反射来判断类型
func Sprint(x interface{}) string {
return doPrint(reflect.ValueOf(x))
}
func doPrint(v reflect.Value) string {
switch v.Kind() {
case reflect.Invalid:
return "invalid"
case reflect.Int, reflect.Int8: // 省略其他长度的类型
return strconv.FormatInt(v.Int(), 10)
// ... 为简化起见,省略浮点数和复数分支
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.String:
return strconv.Quote(v.String())
// 对于引用类型,输出他们的类型以及地址
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
return v.Type().String() + " 0x" + strconv.FormatUint(uint64(v.Pointer()), 16)
// reflect.Array, reflect.Struct, reflect.Interface
default:
return v.Type().String() + " value"
}
}
对于聚合类型(结构体和数组)以及接口,它只输出值的类型;对于引用类型(通道、函数、指针、slice和map),它输出了类型和以十六进制表示的引用地址。
使用示例:
var x = 1
fmt.Println(Sprint(x)) // 1
fmt.Println(Sprint([]int{x})) // []int 0xc00000a330
更细致的用法
func Print(v reflect.Value) (s string) {
switch v.Kind() {
case reflect.Invalid:
return "invalid\n"
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
s += fmt.Sprintf("%v", v.Index(i)) + ", "
}
return
case reflect.Struct:
for i := 0; i< v.NumField(); i++ {
s += fmt.Sprintf("%v=%v, ", v.Type().Field(i).Name, v.Field(i))
}
return
case reflect.Map:
for _, key := range v.MapKeys() {
s += fmt.Sprintf("%v=%v, ", key, v.MapIndex(key))
}
return
case reflect.Ptr:
if v.IsNil() {return "nil"}
return fmt.Sprintf("%v", v.Elem())
case reflect.Interface:
if v.IsNil() {return ""}
return fmt.Sprintf("type=%v, value=%v", v.Elem().Type(), v.Elem())
default:
return fmt.Sprintf("%v", v)
}
}
调用这个函数,得到如下输出:
a := []int{1, 2, 3, 4, 5}
s := &student{"Jiafu", 123456}
m := map[string]interface{}{"name": "jiafu", "id": 123456}
fmt.Println(Print(reflect.ValueOf(a))) // 1, 2, 3, 4, 5,
fmt.Println(Print(reflect.ValueOf(*s))) // username=Jiafu, id=123456,
fmt.Println(Print(reflect.ValueOf(m))) // name=jiafu, id=123456,
fmt.Println(Print(reflect.ValueOf(s))) // {Jiafu 123456}
接下来对上面实现的分支逐一进行讲解:
- Slice与数组:两者的逻辑一致。
Len()方法会返回slice或数组中元素的个数,Index(i)会返回第i个元素,返回的元素类型为reflect.Value。尽管reflect.Value有很多方法,但对于每个值,只有少量的方法可以安全调用。比如Index(i)方法可以在Slice、Array和String类型的值上安全调用,对于其他类型则会引起崩溃。 - 结构体:
NumFiled()方法可以输出得到字段数,Field(i)会返回第i个字段,返回的类型为reflect.Value。如需获取字段名称,则需要先获得结构体的reflect.Type才能获得第i个字段的名称。 - Map:
MapKeys()方法返回一个元素类型为reflect.Value的slice,每个元素都是一个map的建。与平常遍历map类似的是,顺序是不固定的。MapIndex(key)返回key对应的值。 - 指针:
Elem()方法返回指针指向的变量,同样也是以reflect.Value类型返回。当指针是nil时,返回的结果属于Invalid类型。 - 接口:通过
Elem()方法来获取动态值,进一步输出它的类型和值。
注意!即使是非导出字段,在反射下也是可见的。
小结
- 通过
reflect.TypeOf()函数可以获得对象的reflect.Type类型对象。 - 通过
reflect.ValueOf()函数可以获得对象的reflect.Value值对象。 - 通过
reflect.Type对象的成员方法Kind()可以获得reflect.Kind种类对象。 - Go语言将所有类型的变量划分成了27个大类,根据
Kind进行判断,即可对所有的GO对象进行分类处理。 reflect.Type中的很多方法都是与特定类型绑定的,调用不属于绑定类型的方法会报错。
本篇文章仅仅介绍了一下反射的基础知识,以及如何利用反射来解析一个变量的类型和值。如何改变值将放在后面的文章中进行讲解。