Go基础:反射介绍及类型的判断

4,879 阅读7分钟

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 "???"
	}
}

简单的写了几个之后,我们发现,我们需要对每一种类型都做一个详细实现。更可怕的是,对于[]float64map[string]string等其他的类型,甚至有无限种,我们总不能添加无限多的分支吧。更何况还有自己命名的类型。

当我们无法了解一个未知类型的布局时,这段代码就无法继续实现它的功能了,这时,我们就需要反射了。

反射介绍

反射(reflection)是在 Java出现后迅速流行起来的一种概念,通过反射可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。

Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。

反射也可以让我们将类型本身作为第一类的值类型处理。

Go语言中的反射是由reflect包提供支持的,它定义了两个重要的类型TypeValue任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,并且reflect包提供了 reflect.TypeOf()reflect.ValueOf()两个函数来获取任意对象的ValueType

类型对象

使用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的类型对象tt的类型为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对象之后,好像对我们解决之前的问题并没有什么帮助,不用着急,以TypeValue为基础,接下来介绍的种类对象(Kind)将大展身手。

我们需要区分一个对象大的种类的时候,就会用种类对象(Kind)。

类型(Type)指的是系统原生数据类型,如intstringboolfloat32等类型,以及使用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的分类只有少数的几种:基础类型BoolString以及各种数字类型;聚合类型ArrayStruct;引用类型ChanFuncPtrSliceMap、接口类型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个字段的名称。
  • MapMapKeys()方法返回一个元素类型为reflect.Value的slice,每个元素都是一个map的建。与平常遍历map类似的是,顺序是不固定的。MapIndex(key)返回key对应的值。
  • 指针Elem()方法返回指针指向的变量,同样也是以reflect.Value类型返回。当指针是nil时,返回的结果属于Invalid类型。
  • 接口:通过Elem()方法来获取动态值,进一步输出它的类型和值。

注意!即使是非导出字段,在反射下也是可见的。

小结

  1. 通过reflect.TypeOf()函数可以获得对象的reflect.Type类型对象。
  2. 通过reflect.ValueOf()函数可以获得对象的reflect.Value值对象。
  3. 通过reflect.Type对象的成员方法Kind()可以获得reflect.Kind种类对象。
  4. Go语言将所有类型的变量划分成了27个大类,根据Kind进行判断,即可对所有的GO对象进行分类处理。
  5. reflect.Type中的很多方法都是与特定类型绑定的,调用不属于绑定类型的方法会报错。

本篇文章仅仅介绍了一下反射的基础知识,以及如何利用反射来解析一个变量的类型和值。如何改变值将放在后面的文章中进行讲解。