Go 面向对象编程篇:空接口、反射和泛型

空接口的引入

熟悉 Java 的同学应该都知道,在这个号称血统最纯正的面向对象编程语言中,「万事万物皆对象」,并且所有类都继承自祖宗类「Object」,所以 Object 类型变量可以指向任何类的实例。

Go语言打破了传统面向对象编程中类与类之间继承的概念,而是通过组合实现方法和属性的复用,所以不存在类似的继承关系数,也就没有所谓的祖宗类,而且类与接口之间也不再通过implements 关键字强制绑定实现关系,所以 Go 语言的面向对象编程非常灵活。

在Go语言中,类与接口的实现关系是通过类所实现的方法在编译期推断出来的,如果我们定义一个空接口的话,那么显然所有的类都实现了这个接口,反过来,我们也可以通过空接口来指向任意类型,从而实现类似Java中Object类所承担的功能,而且显然Go的空接口实现更加简洁,通过一个简单的字面量即可完成:

interface{}

需要注意的是空接口和接口零值不是一个概念,前者是interface{},后者是nil

空接口的基本使用

指向任意类型变量

我们可以将其指向基本类型:

var v1 interface{} = 1 // 将 int 类型赋值给 interface{} 
var v2 interface{} = "学院君" // 将 string 类型赋值给 interface{} 
var v3 interface{} = true  // 将 bool 类型赋值给 interface{}

也可以将其指向复合类型:

var v4 interface{} = &v2 // 将指针类型赋值给 interface{} 
var v5 interface{} = []int{1, 2, 3}  // 将切片类型赋值给 interface{} 
var v6 interface{} = struct{   // 将结构体类型赋值给 interface{}
    id int
    name string
}{1, "学院君"} 
声明任意类型参数

空接口最典型的使用场景就是用于声明函数支持任意类型的参数,比如 Go 语言标准库 fmt 中的打印函数就是这样实现的:

func Printf(fmt string, args ...interface{}) 
func Println(args ...interface{}) ...
func (p *pp) printArg(arg interface{}, verb rune)

反射

很多现代高级编程语言都提供了对反射的支持,通过反射,你可以在运行时动态获取变量的类型和结构信息,然后基于这些信息做一些非常灵活的工作,一个非常典型的反射应用场景就是 IoC 容器。

Go 也支持反射功能,并且专门提供了一个 reflect 包用于提供反射相关的 API,Go 格式化输出标准库 fmt 底层就大量使用了反射。

reflect 包提供的两个最常用、最重要的类型就是 reflect.Typereflect.Value。前者用于表示变量的类型,后者用于存储任何类型的值,分别可以通过 reflect.TypeOfreflect.ValueOf 函数获取。

使用示例

以前面编写的 Dog 类为例,我们可以这样在运行时通过反射获取其类型:

animal := NewAnimal("中华田园犬")
pet := NewPet("泰迪")
dog := NewDog(&animal, pet)

// 返回的是 reflect.Type 类型值
dogType := reflect.TypeOf(dog)    
fmt.Println("dog type:", dogType)

执行这段代码,打印结果是:

dog type: animal.Dog

如果你想要获取 dog 值的结构体信息,并且动态调用其成员方法,使用反射的话需要先获取对应的 reflect.Value 类型值:

// 返回的是 dog 指针对应的 reflect.Value 类型值
dogValue := reflect.ValueOf(&dog).Elem()

当然,Dog 类中不包含指针方法的话,也可以返回 dog 值对应的 reflect.Value 类型值:

dogValue := reflect.ValueOf(dog)

我们可以通过反射获取变量的所有未知结构信息,以结构体为例(基本类型只有类型和值,更加简单),包括其属性、成员方法的名称和类型,值和可见性,还可以动态修改属性值以及调用成员方法。

不过这种灵活是有代价的,因为所有这些解析工作都是在运行时而非编译期间进行,所以势必对程序性能带来负面影响,而且可以看到,反射代码的可读性和可维护性比起正常调用差很多,最后,反射代码出错不能在构建时被捕获,而是在运行时以恐慌的形式报告,这意味着反射错误有可能使你的程序崩溃。

所以,如果有其他更好解决方案的话,尽量不要使用反射。

基于空接口和反射实现泛型

不过,在某些场景下,目前只能使用反射来实现,比如泛型,因为现在 Go 官方尚未在语法层面提供对泛型的支持,我们只能通过空接口结合反射来实现。

本帖子简单演示过 Go 泛型的实现,这里再更严谨地实现下。

空接口 interface{} 本身可以表示任何类型,因此它其实就是一个泛型了,不过这个泛型太泛了,我们必须结合反射在运行时对实际传入的参数做类型检查,让泛型变得可控,从而确保程序的健壮性,否则很容易因为传递进来的参数类型不合法导致程序崩溃。

下面我们通过一个自定义容器类型的实现来演示如何基于空接口和反射来实现泛型:

package main

import (
	"fmt"
	"reflect"
)

type Container struct {
	s reflect.Value
}

// 通过传入存储元素类型和容量来初始化容器
func NewContainer(t reflect.Type, size int) *Container {
	if size <= 0  {
		size = 64
	}
	// 基于切片类型实现这个容器,这里通过反射动态初始化这个底层切片
	return &Container{
		s: reflect.MakeSlice(reflect.SliceOf(t), 0, size),
	}
}

// 添加元素到容器,通过空接口声明传递的元素类型,表明支持任何类型
func (c *Container) Put(val interface{})  error {
	// 通过反射对实际传递进来的元素类型进行运行时检查,
	// 如果与容器初始化时设置的元素类型不同,则返回错误信息
	// c.s.Type() 对应的是切片类型,c.s.Type().Elem() 应的才是切片元素类型
	if reflect.ValueOf(val).Type() != c.s.Type().Elem() {
		return fmt.Errorf("put error: cannot put a %T into a slice of %s",
			val, c.s.Type().Elem())
	}
	// 如果类型检查通过则将其添加到容器中
	c.s = reflect.Append(c.s, reflect.ValueOf(val))
	return nil
}

// 从容器中读取元素,将返回结果赋值给 val,同样通过空接口指定元素类型
func (c *Container) Get(val interface{}) error {
	// 还是通过反射对元素类型进行检查,如果不通过则返回错误信息
	// Kind 与 Type 相比范围更大,表示类别,如指针,而 Type 则对应具体类型,如 *int
	// 由于 val 是指针类型,所以需要通过 reflect.ValueOf(val).Elem() 获取指针指向的类型
	if reflect.ValueOf(val).Kind() != reflect.Ptr ||
		reflect.ValueOf(val).Elem().Type() != c.s.Type().Elem() {
		return fmt.Errorf("get error: needs *%s but got %T", c.s.Type().Elem(), val)
	}
	// 将容器第一个索引位置值赋值给 val 指针
	reflect.ValueOf(val).Elem().Set( c.s.Index(0) )
	// 然后删除容器第一个索引位置值
	c.s = c.s.Slice(1, c.s.Len())
	return nil
}

func main() {
	nums := []int{1, 2, 3, 4, 5}

	// 初始化容器,元素类型和 nums 中的元素类型相同
	c := NewContainer(reflect.TypeOf(nums[0]), 16)

	// 添加元素到容器
	for _, n := range nums {
		if err := c.Put(n); err != nil {
			panic(err)
		}
	}

	// 从容器读取元素,将返回结果初始化为 0
	num := 0
	if err := c.Get(&num);
	err != nil{
		panic(err)
	}

	// 打印返回结果值
	fmt.Printf("%v (%T)\n", num, num)
}

具体细节都已经在代码注释中详细标注了,执行上述代码,打印结果如下:

1 (int)

如果我们试图添加其他类型元素到容器:

if err := c.Put("s"); err != nil {
    panic(err)
}

或者存储返回结果的变量类型与容器内元素类型不符:

if err := c.Get(num); err != nil {
    panic(err)
}

都会报错:

在这里插入图片描述

在这里插入图片描述

空结构体

另外,有的时候你可能会看到空的结构体类型定义:

struct{}

表示没有任何属性和成员方法的空结构体,该类型的实例值只有一个,那就是 struct{}{},这个值在 Go 程序中永远只会存一份,并且占据的内存空间是 0,当我们在并发编程中,将通道(channel)作为传递简单信号的介质时,使用 struct{} 类型来声明最好不过。

相关文章

类型转换 1、int转string 2、string转int 3、string转float ...
package main import s &quot;strings&quot; import...
类使用:实现一个people中有一个sayhi的方法调用功能,代码如...
html代码: beego代码:
1、读取文件信息: 2、读取文件夹下的所有文件: 3、写入文件...
配置环境:Windows7+推荐IDE:LiteIDEGO下载地址:http:...