空接口的引入

熟悉 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 intname 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 mainimport ("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)}}// 从容器读取元素,将返回结果初始化为 0num := 0if 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{} 类型来声明最好不过。

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

  1. Java面向对象编程篇3——接口与抽象类

    Java面向对象编程篇3--接口与抽象类 1.接口(interface) 接口中可以含有变量和方法.但是要注意,接口中的变量会被隐式地指定为public static final变量(并且只能是pub ...

  2. Java面向对象编程篇6——注解与反射

    Java面向对象编程篇6--注解与反射 1.注解概述 Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制 Java 语言中的类.方法.变量.参数和包等都可 ...

  3. Java面向对象编程篇1——类与对象

    Java面向对象编程篇1--类与对象 1.面向过程 1.1.概念 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了 1.2.优缺点 优点:性 ...

  4. Java面向对象编程篇4——内部类

    Java面向对象编程篇4--内部类 1.内部类的概念 当一个类的定义出现在另外一个类的类体中时,那么这个类叫做内部类 (Inner),而这个内部类所在的类叫做外部类(Outer). 类中的内容:成员变 ...

  5. Java面向对象编程篇2——面向对象三大特点

    Java面向对象编程篇2--面向对象三大特点 1.封装 1.1.封装的概念 通常情况下可以在测试类给成员变量赋值一些合法但不合理的数值,无 论是编译阶段还是运行阶段都不会报错或者给出提示,此时与现实生 ...

  6. 为什么有人说面向对象编程就是面向接口编程?

    "面向对象编程就是面向接口编程" 这句话相信, 很多人都在网上见过, 装b利器. 我一开始也是这么想的, 那些装b者丢下这一句, 就没下文了. 首先, 我认为这句话是1个假命题. ...

  7. Java面向对象编程篇5——枚举

    Java面向对象编程篇5--枚举 1.枚举的概念 在日常生活中这些事物的取值只有明确的几个固定值,此时描述这些事 物的所有值都可以一一列举出来,而这个列举出来的类型就叫做枚举类型 2.枚举的定义 使用 ...

  8. Java面向对象编程——抽象类和接口

    Java面向对象编程--抽象类和接口 定义类的过程就是抽象和封装的过程,而抽象类与接口则是对实体类进行更高层次的抽象,进定义公共行为和特征. 抽象类: 如果一个类没有足够的信息去描述一个具体的对象,那 ...

  9. 从面向对象编程转为面向接口编程

    大家写过C++或者Java,或者其他语言,基本上都会接触到面向对象这个概念. 面向对象,本身是软件编程发展过程中的产物,当然相比于面向过程,是一种突破性的设计.但是,如果只是停留在面向对象编程,而不是 ...

  10. 面向对象编程原则(07)——接口隔离原则

    版权声明 本文原创作者:谷哥的小弟 作者博客地址:http://blog.csdn.net/lfdfhl 参考资料 <大话设计模式> 作者:程杰 <Java设计模式> 作者:刘 ...

最新文章

  1. css z-index 的使用
  2. mplus 软件_Mplus 7.4 软件及代码
  3. 十一、Powerbi函数篇
  4. Link-Cut Tree指针模板
  5. 2021年高考成绩查询襄阳状元,大胆猜测一下,2021年高考,湖北省文理状元会花落谁家?...
  6. LeetCode 1832. 判断句子是否为全字母句
  7. python re模块compile_Python re模块的match方法
  8. ASP.NET弹出对话框并跳转页面
  9. java的关键字和保留字_Java关键字和保留字及其含义
  10. 关于ExtJs4的Grid带 查询 参数 分页(baseParams--extraParams)
  11. jdbc连接mysql正规方法_JDBC基础篇(MYSQL)——通过JDBC连接数据库的三种方式
  12. repo forall -c 用法
  13. 过程FMEA步骤四:失效分析(一)
  14. 苹果计算机打音乐,给苹果手机“隔空投送”更多的音乐和文件!
  15. 一位老电子工程师的十年职场感悟
  16. 感应(异步)电机磁场定向控制速度环PI控制参数设计
  17. 【华为OD机试Python实现】HJ70 矩阵乘法计算量估算(中等)
  18. WiFiDisplay
  19. STM32单片机和51单片机有何区别?
  20. 宝塔面板无法连接FTP空间解决方法(超详细)

热门文章

  1. linux基础指令练习
  2. Opencv4.5.2 与vs2019的安装问题与代码测试
  3. pycharm有效期
  4. 【MySQL学习笔记】第14章 使用文件进行交互(二 SQL命令交互)
  5. UE4和C++ 开发-C++项目中怎么添加灯光?-第一篇:方法一
  6. 利用cad计算型材的弹性模量_常用材料弹性模量
  7. IIS怎么安装SSL域名证书?
  8. 中日韩、纯英文都可以用OCR识别
  9. 冬令营第一周工作总结及计划表
  10. python 爬虫爬出来为什么都是空的_为什么铺天盖地都是python的广告?