一、空接口的引入

Object
implements
Object
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)

关于这一点,我们在前面类型断言中已经演示过。

实现更灵活的类型断言

此外,我们还可以基于空接口来实现更加灵活的类型断言。

.IAnimal
var animal = NewAnimal("中华田园犬")
var pet = NewPet("泰迪")
var any interface{} = NewDog(&animal, pet)
if dog, ok := any.(Dog); ok {
    fmt.Println(dog.GetName())
    fmt.Println(dog.Call())
    fmt.Println(dog.FavorFood())
    fmt.Println(reflect.TypeOf(dog))
}

三、反射

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

reflectfmt
reflectreflect.Typereflect.Valuereflect.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
dogreflect.Value
// 返回的是 dog 指针对应的 reflect.Value 类型值
dogValue := reflect.ValueOf(&dog).Elem()
Dogdogreflect.Value
dogValue := reflect.ValueOf(dog)
dog
// 获取 dogValue 的所有属性
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))
}
// 获取 dogValue 的所有方法
fmt.Println("================ Methods ================")
for j := 0; j < dogValue.NumMethod(); j++ {
    // 获取方法名
    fmt.Println("name:", dogValue.Type().Method(j).Name)
    // 获取方法类型
    fmt.Println("type:", dogValue.Type().Method(j).Type)
    // 调用该方法
    fmt.Println("exec result:", dogValue.Method(j).Call([]reflect.Value{}))
}

执行上述代码,对应的打印结果如下:

Dog

具体每个反射函数的语法细节,可以参考 Go 官方提供的 reflect 包文档,这里就不一一展开了。

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

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

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

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

不过,在某些场景下,目前只能使用反射来实现,比如泛型,因为现在 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)
}

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

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

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

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

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

都会报错:

注:本节完整示例代码可以在 Github 代码仓库获取:nonfu/golang-tutorial。

在上面这段代码中,为了提高程序的健壮性,我们引入了错误处理机制,这块内容我们即将在下个章节中详细给大家介绍。

四、空结构体

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

struct{}
struct{}{}0struct{}

(本文完)