golang基础(34.空接口,反射,泛型)
空接口的引入
Go 语言打破了传统面向对象编程中类与类之间继承的概念,而是通过组合实现方法和属性的复用,所以不存在类似的继承关系树,也就没有所谓的祖宗类,而且类与接口之间也不再通过 implements 关键字强制绑定实现关系,所以 Go 语言的面向对象编程非常灵活。在 Go 语言中,类与接口的实现关系是通过类所实现的方法在编译期推断出来的,如果我们定义一个空接口的话,那么显然所有的类都实现了这个接口,反过来,我们也可以通过空接口来指向任意类型,从而实现类似 Java 中 Object 类所承担的功能,而且显然 Go 的空接口实现更加简洁,通过一个简单的字面量即可完成:
interface{}
:::warning 需要注意的是空接口和接口零值不是一个概念,前者是 interface{},后者是 nil。 :::
空接口的基本使用
指向任意类型
package main
func main() {
var a interface{} = 1
var b interface{} = "sss"
var c interface{} = 1.11
var d interface{} = true
var e interface{} = []int{1, 2, 3}
var f interface{} = [2]int{1, 2}
var g interface{} = struct {
id int
}{id: 1}
}
声明任意类型参数
func test(args ...interface{}) {
for _, arg := range args {
fmt.Println(arg)
}
}
实现更灵活的类型断言我们提到类型断言运算符 . 左侧的变量必须是接口类型,而空接口可以表示任何类型,所以我们可以基于空接口将其他类型变量转化为空接口类型,这样,就不必单独引入 IAnimal 接口了:
dog2 := Dog{}
var f interface{} = dog2
if dog3, ok := f.(Dog); ok {
fmt.Println(dog3)
}
反射
反射是语言的高级特性,通过反射,你可以在运行时动态获取变量的类型和结构信息,然后基于这些信息做一些非常灵活的工作,一个非常典型的反射应用场景就是 IoC 容器。Go 也支持反射功能,并且专门提供了一个 reflect 包用于提供反射相关的 API。reflect 包提供的两个最常用、最重要的类型就是 reflect.Type 和 reflect.Value。前者用于表示变量的类型,后者用于存储任何类型的值,分别可以通过 reflect.TypeOf 和 reflect.ValueOf 函数获取。通过反射回去结构体类型
package main
import (
"fmt"
"reflect"
)
type Dog struct {
name string
}
func main() {
dog := Dog{
name: "dog",
}
// 获取结构体类型
fmt.Println("type:", reflect.TypeOf(dog))
}
如果你想要获取 dog 值的结构体信息,并且动态调用其成员方法,使用反射的话需要先获取对应的 reflect.Value 类型值:
// 返回的是 dog 指针对应的 reflect.Value 类型值
fmt.Println("elems",reflect.TypeOf(dog).Elem())
package main
import (
"fmt"
"reflect"
)
type Dog struct {
name string
}
func (d Dog) Call() {
fmt.Println("你在狗叫什么")
}
func main() {
dog := &Dog{
name: "dog",
}
// 获取结构体类型
fmt.Println("type:", reflect.TypeOf(dog))
// 返回的是 dog 指针对应的 reflect.Value 类型值
fmt.Println("elems", reflect.ValueOf(dog))
dogValue := reflect.ValueOf(*dog)
fmt.Println("================ Props ================")
for i := 0; i < dogValue.NumField(); i++ {
// 获取属性名
fmt.Println("name:", dogValue.Type().Field(i).Name)
// 获取属性类型
fmt.Println("type:", dogValue.Type().Field(i).Type)
// 获取属性值
fmt.Println("value:", dogValue.Field(i))
}
fmt.Println("================ Methods ================")
for i := 0; i < dogValue.NumMethod(); i++ {
fmt.Println("type:", dogValue.Type().Method(i).Type)
fmt.Println("name", dogValue.Type().Method(i).Name)
// 调用该方法
fmt.Println("exec result:", dogValue.Method(i).Call([]reflect.Value{}))
}
}
可以看到,即便我们不知道 Dog 类的属性类型、成员方法细节时,依然可以通过反射来动态获取和调用,非常灵活。具体每个反射函数的语法细节,可以参考 Go 官方提供的 reflect 包文档,这里就不一一展开了。我们可以通过反射获取变量的所有未知结构信息,以结构体为例(基本类型只有类型和值,更加简单),包括其属性、成员方法的名称和类型,值和可见性,还可以动态修改属性值以及调用成员方法。不过这种灵活是有代价的,因为所有这些解析工作都是在运行时而非编译期间进行,所以势必对程序性能带来负面影响,而且可以看到,反射代码的可读性和可维护性比起正常调用差很多,最后,反射代码出错不能在构建时被捕获,而是在运行时以恐慌的形式报告,这意味着反射错误有可能使你的程序崩溃。所以,如果有其他更好解决方案的话,尽量不要使用反射。
基于空接口和反射实现泛型
空接口 interface{} 本身可以表示任何类型,因此它其实就是一个泛型了,不过这个泛型太泛了,我们必须结合反射在运行时对实际传入的参数做类型检查,让泛型变得可控,从而确保程序的健壮性,否则很容易因为传递进来的参数类型不合法导致程序崩溃。下面我们通过一个自定义容器类型的实现来演示如何基于空接口和反射来实现泛型:
package main
import (
"fmt"
"reflect"
)
type Container struct {
s reflect.Value
}
//NewContainer 通过传入存储元素类型和容量来初始化容器
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
}
//Get 从容器中读取元素,将返回结果赋值给 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)
", num, num)
}
空结构体
另外,有的时候你可能会看到空的结构体类型定义:
struct{}
表示没有任何属性和成员方法的空结构体,该类型的实例值只有一个,那就是 struct{}{},这个值在 Go 程序中永远只会存一份,并且占据的内存空间是 0,当我们在并发编程中,将通道(channel)作为传递简单信号的介质时,使用 struct{} 类型来声明最好不过。