「... 语法糖」&「万能类型」

... 用法

在 Go 中...是一种语法糖,极大程度的方便程序的书写,下面先介绍几种常见用法!!

万能类型

在 Java 中,所有类都继承自Object,根据多态的特性,可以将子类赋值给父类,也可以将实现了某个接口的对象赋值给该接口

在 Go 中,不存在extends关键字用于继承某个类,也不存在implements关键字用于实现某个接口

Go 中的继承,更像是一种组合关系,在子类中添加一个父类对象即可完成继承的功能,所以 Go 中继承没有多态,也就是无法将子类对象赋值给父类

Go 中的实现是隐式定义的!!假如一个接口中有三个方法,一个类中也包含这三个方法,那么就称该类实现了这个接口,Go 中的接口是有多态的

从上面代码可以看出,showAnimal(animal AnimalIF)方法的参数是AnimalIF接口,而调用该方法的两个地方传入的是CatDog指针,这正是多态的体现!!

注意:这里总结几个需要关注的重点!!

介绍了 GO 中的继承和实现,那万能类型到底是什么呢???

万能类型其实就是一个空接口,不含任何方法的接口,所以可以把任何对象赋值给空接口对象,和 Java 中的Object很像

秉着不到底层不死心的原则,下面再来看看空接口的底层结构:

下面给一个小例子:

首先,接口可以通过断言判断其动态类型,说明接口中保存了赋值变量的类型,即:_type *_type

其次,inter1保存的是num,其中类型为int,数据为3,所以修改inter1的值,并不会影响num的值

同样的,inter2保存的是&num,其中类型为*int,数据为0x1400000e0d8,是num变量的地址,所以修改inter2保存地址的值,会影响num的值

坑~

介绍完了...语法糖和万能类型,下面引出这两个结合在一起带来的一个坑。先看一个例子:

上面例子的报错很明显的,Go 对类型比较严格,传入的参数类型是[]int8,而函数所需的参数是[]int,所以类型不匹配,直接报错!!

下面再给一个例子:

对于f(2, 5, 8)的调用方式,在调用函数时,参数会转换成[]interface{2, 5, 8}的切片

对于f([]int{2, 5, 8}),的调用方式,在调用函数时,参数会转换成[][]interface{}{{2, 5, 8}}的二维切片

那为什么将切片打散的第三种调用方式f([]int{2, 5, 8}...)会报错呢?难道不能等价于第一种调用方式嘛?

就算将[]int类型打散,但是其底层类型依然还是[]int,所以这个问题变成了为什么[]int不能赋值给[]interface{},不是说万能类型可以接收一切嘛?

其实在 Go 的官网上给出了这个问题的解释,详情可见 Go Wiki: InterfaceSlice

简单的总结一下官网给的解释:

其实看完解释的两个原因,并不能完全消除我心中的疑惑,请看下一节对底层实现的分析!!

底层实现

在 Go 中,可以通过go build -gcflags -S main.go命令输出对应的汇编代码,首先给出一段源代码:

上述源代码对应的汇编代码的核心部分如下:

从汇编代码可以看出,源代码的第 6 行主要是创建了一个[]int对象,并将2, 5, 8放入其中,函数f()也是将可变长参数视为切片处理

感兴趣的同学可以去试试,将第 6 行代码换成f([]int{2, 5, 8}...)后,生成的汇编代码和上面的一模一样

不仅如此,将第 6 行代码换成f([]int{2, 5, 8}),第 8 行代码换成func f(arr []int)后,生成的汇编代码也和上面的一模一样

到此为止,我们搞清楚了 Go 底层到底是如何处理切换类型及可变长类型的参数,下面再看一段源代码:

首先可以确定的是上面的代码可以正确运行,下面生成对应的汇编代码的核心部分如下:

从汇编代码中可以看出对第 6 行代码的处理细节,相同的是也创建了一个切片,不同的是切片内的元素为interface{}类型,每个元素包含类型和数据部分

如果将第 6 行代码换成f([]int8{2, 5, 8}...),那么会创建一个[]int类型的切片,而f(arr ...interface{})所需的参数是[]interface{}

在创建[]int[]interface{}时内存布局是不同的 (到此,终于形成闭环,理解了官网给的解释),所以不能把[]int赋值给[]interface{}

参考文章