在 Go 中...
是一种语法糖,极大程度的方便程序的书写,下面先介绍几种常见用法!!
xxxxxxxxxx
// 定义数组 (切记是数组,并非切片)
// 下面两种方式等价,变量 a,b 均为大小为 3 的数组,即:[3]int
a := [...]int{1, 2, 3}
b := [3]int{1, 2, 3}
// 可变长参数
func f(a ...int) {
fmt.Printf("%T\n", a) // 变量 a 的类型是切片,即:[]int
}
func main() {
f(2, 5, 8, 10) // 调用方式一
f([]int{2, 5, 8}...) // 调用方式二 (此处 ... 用法表示打散切片)
}
在 Java 中,所有类都继承自Object
,根据多态的特性,可以将子类赋值给父类,也可以将实现了某个接口的对象赋值给该接口
在 Go 中,不存在extends
关键字用于继承某个类,也不存在implements
关键字用于实现某个接口
Go 中的继承,更像是一种组合关系,在子类中添加一个父类对象即可完成继承的功能,所以 Go 中继承没有多态,也就是无法将子类对象赋值给父类
xxxxxxxxxx
type Parent struct {
// 父结构体的字段
}
type Child struct {
Parent // 嵌套父结构体
// 子结构体的字段
}
func main() {
c := Child{} // 创建一个 Child 对象
// p := Parent(c) // 报错
}
Go 中的实现是隐式定义的!!假如一个接口中有三个方法,一个类中也包含这三个方法,那么就称该类实现了这个接口,Go 中的接口是有多态的
xxxxxxxxxx
// AnimalIF 动物接口,包含三个方法
type AnimalIF interface {
Sleep()
GetColor() string
GetType() string
}
// Cat 猫
// 由于 Cat 中包含 AnimalIF 接口中的三个方法,所以 Cat 实现了 AnimalIF 接口
type Cat struct {
color string
}
func (c *Cat) Sleep() {
fmt.Println("Cat is Sleep")
}
func (c *Cat) GetColor() string {
return c.color
}
func (c *Cat) GetType() string {
return "Cat"
}
// Dog 狗
// 由于 Dog 中包含 AnimalIF 接口中的三个方法,所以 Dog 实现了 AnimalIF 接口
type Dog struct {
color string
}
func (d *Dog) Sleep() {
fmt.Println("Dog is Sleep")
}
func (d *Dog) GetColor() string {
return d.color
}
func (d *Dog) GetType() string {
return "Dog"
}
func showAnimal(animal AnimalIF) {
animal.Sleep()
fmt.Println(animal.GetColor())
fmt.Println(animal.GetType())
}
func main() {
showAnimal(&Cat{"黄"})
showAnimal(&Dog{"黑"})
}
从上面代码可以看出,showAnimal(animal AnimalIF)
方法的参数是AnimalIF
接口,而调用该方法的两个地方传入的是Cat
和Dog
指针,这正是多态的体现!!
注意:这里总结几个需要关注的重点!!
结构体方法对应的接收者可以是普通对象,也可以是指针对象,但建议一个结构体所有方法的接收者保持一致,要么都是普通对象,要么都是指针对象
无论方法接收者是普通对象,还是指针对象,都可以通过普通对象或指针对象调用方法,唯一的区别在于若方法接收者为指针对象,那么方法调用者可以获得调用方法内的修改
如果接收者为普通对象,方法参数为接口类型,那么调用方法时既可以传普通对象,也可以传指针对象;如果接收者为指针对象,那么调用方法时只能传指针对象
介绍了 GO 中的继承和实现,那万能类型到底是什么呢???
万能类型其实就是一个空接口,不含任何方法的接口,所以可以把任何对象赋值给空接口对象,和 Java 中的Object
很像
xxxxxxxxxx
// any 和 interface{} 等价,都可以表示空接口
type any = interface{}
var a1 interface{} // 定义一个空接口变量 a1
var a2 any // 定义一个空接口变量 a2
秉着不到底层不死心的原则,下面再来看看空接口的底层结构:
xxxxxxxxxx
// src/runtime/runtime2.go
type eface struct {
_type *_type // 类型指针
data unsafe.Pointer // 数据指针
}
下面给一个小例子:
xxxxxxxxxx
func main() {
num := 3 // 类型 int,值 3
fmt.Printf("num 地址为:%p\n", &num)
var inter1 interface{} = num
var inter2 interface{} = &num
fmt.Printf("修改 inter1 之前,inter1 = %v, num = %v\n", inter1, num)
inter1 = 10
fmt.Printf("修改 inter1 之后,inter1 = %v, num = %v\n", inter1, num)
fmt.Printf("修改 inter2 之前,inter2 = %v, num = %v\n", inter2, num)
t2 := inter2.(*int)
*t2 = 20
fmt.Printf("修改 inter2 之后,inter2 = %v, num = %v\n", inter2, num)
}
// num 地址为:0x1400000e0d8
// 修改 inter1 之前,inter1 = 3, num = 3
// 修改 inter1 之后,inter1 = 10, num = 3
// 修改 inter2 之前,inter2 = 0x1400000e0d8, num = 3
// 修改 inter2 之后,inter2 = 0x1400000e0d8, num = 20
首先,接口可以通过断言判断其动态类型,说明接口中保存了赋值变量的类型,即:_type *_type
其次,inter1
保存的是num
,其中类型为int
,数据为3
,所以修改inter1
的值,并不会影响num
的值
同样的,inter2
保存的是&num
,其中类型为*int
,数据为0x1400000e0d8
,是num
变量的地址,所以修改inter2
保存地址的值,会影响num
的值
介绍完了...
语法糖和万能类型,下面引出这两个结合在一起带来的一个坑。先看一个例子:
xxxxxxxxxx
func f(a ...int) {
fmt.Printf("%T\n", a)
}
func main() {
f(2, 5, 8, 10)
f([]int{2, 5, 8}...)
// f([]int8{2, 5, 8}...) 报错
}
上面例子的报错很明显的,Go 对类型比较严格,传入的参数类型是[]int8
,而函数所需的参数是[]int
,所以类型不匹配,直接报错!!
下面再给一个例子:
xxxxxxxxxx
func f(arr ...interface{}) {
fmt.Printf("%+v\n", arr)
}
func main() {
f(2, 5, 8)
f([]int{2, 5, 8})
// f([]int{2, 5, 8}...) 报错
}
对于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
简单的总结一下官网给的解释:
万能类型是interface{}
,而并非[]interface{}
,所以[]int
可以赋值给interface{}
,但不能赋值给[]interface{}
从内存的角度,假设长度为N
的[]interface{}
,那么所占空间就是N*2
,因为一个interface{}
中含有两个指针;而对于长度为N
的[]int
,那么所占空间就是N*sizeof(MyType)
其实看完解释的两个原因,并不能完全消除我心中的疑惑,请看下一节对底层实现的分析!!
在 Go 中,可以通过go build -gcflags -S main.go
命令输出对应的汇编代码,首先给出一段源代码:
1 package main
2
3 import "fmt"
4
5 func main() {
6 f(2, 5, 8)
7 }
8 func f(arr ...int) {
9 fmt.Printf("%+v\n", arr)
10 }
上述源代码对应的汇编代码的核心部分如下:
xxxxxxxxxx
; ......
0x0018 00024 (main.go:6) MOVD $type:[3]int(SB), R0 ; 将 []int 类型放入寄存器 R0 中
0x0020 00032 (main.go:6) PCDATA $1, $0
0x0020 00032 (main.go:6) CALL runtime.newobject(SB) ; 调用 newobject 分配内存,参数在 R0 中,返回值存入寄存器 R0 中
0x0024 00036 (main.go:6) MOVD $2, R1 ; 将 2 放入寄存器 R1 中
0x0028 00040 (main.go:6) MOVD R1, (R0) ; 将寄存器 R1 的值放入寄存器 R0 所指向的地址处
0x002c 00044 (main.go:6) MOVD $5, R1 ; 将 5 放入寄存器 R1 中
0x0030 00048 (main.go:6) MOVD R1, 8(R0) ; 将寄存器 R1 的值放入寄存器 R0 所指向的地址 +8 处
0x0034 00052 (main.go:6) MOVD $8, R1 ; 将 8 放入寄存器 R1 中
0x0038 00056 (main.go:6) MOVD R1, 16(R0) ; 将寄存器 R1 的值放入寄存器 R0 所指向的地址 +16 处
0x003c 00060 (<unknown line number>) NOP
0x003c 00060 (main.go:9) STP (ZR, ZR), main..autotmp_11-16(SP)
0x0040 00064 (main.go:9) MOVD $3, R1
0x0044 00068 (main.go:9) MOVD R1, R2
0x0048 00072 (main.go:9) PCDATA $1, $1
0x0048 00072 (main.go:9) CALL runtime.convTslice(SB) ; 调用 convTslice 创建 slice,参数在 R0 中
0x004c 00076 (main.go:9) MOVD $type:[]int(SB), R1
0x0054 00084 (main.go:9) MOVD R1, main..autotmp_11-16(SP)
0x0058 00088 (main.go:9) MOVD R0, main..autotmp_11-8(SP)
0x005c 00092 (main.go:9) PCDATA $0, $-3
0x005c 00092 (print.go:233) MOVD os.Stdout(SB), R1
从汇编代码可以看出,源代码的第 6 行主要是创建了一个[]int
对象,并将2, 5, 8
放入其中,函数f()
也是将可变长参数视为切片处理
感兴趣的同学可以去试试,将第 6 行代码换成f([]int{2, 5, 8}...)
后,生成的汇编代码和上面的一模一样
不仅如此,将第 6 行代码换成f([]int{2, 5, 8})
,第 8 行代码换成func f(arr []int)
后,生成的汇编代码也和上面的一模一样
到此为止,我们搞清楚了 Go 底层到底是如何处理切换类型及可变长类型的参数,下面再看一段源代码:
xxxxxxxxxx
1 package main
2
3 import "fmt"
4
5 func main() {
6 f(2, 5, 8)
7 }
8 func f(arr ...interface{}) {
9 fmt.Printf("%+v\n", arr)
10 }
首先可以确定的是上面的代码可以正确运行,下面生成对应的汇编代码的核心部分如下:
xxxxxxxxxx
; ......
0x0018 00024 (main.go:6) MOVD $type:[3]interface {}(SB), R0 ; 将 []interface{} 类型放入寄存器 R0 中
0x0020 00032 (main.go:6) PCDATA $1, $0
0x0020 00032 (main.go:6) CALL runtime.newobject(SB) ; 调用 newobject 分配内存,参数在 R0 中,返回值存入寄存器 R0 中
0x0024 00036 (main.go:6) MOVD $type:int(SB), R1 ; 将 int 类型放入寄存器 R1 中
0x002c 00044 (main.go:6) MOVD R1, (R0) ; 将寄存器 R1 的值放入寄存器 R0 所指向的地址处
0x0030 00048 (main.go:6) MOVD $main..stmp_0(SB), R2 ; 将 2 放入寄存器 R2 中
0x0038 00056 (main.go:6) MOVD R2, 8(R0) ; 将寄存器 R2 的值放入寄存器 R0 所指向的地址 +8 处
0x003c 00060 (main.go:6) MOVD R1, 16(R0) ; 将寄存器 R1 的值放入寄存器 R0 所指向的地址 +16 处
0x0040 00064 (main.go:6) MOVD $main..stmp_1(SB), R2 ; 将 5 放入寄存器 R2 中
0x0048 00072 (main.go:6) MOVD R2, 24(R0) ; 将寄存器 R2 的值放入寄存器 R0 所指向的地址 +24 处
0x004c 00076 (main.go:6) MOVD R1, 32(R0) ; 将寄存器 R1 的值放入寄存器 R0 所指向的地址 +32 处
0x0050 00080 (main.go:6) MOVD $main..stmp_2(SB), R1 ; 将 8 放入寄存器 R1 中
0x0058 00088 (main.go:6) MOVD R1, 40(R0) ; 将寄存器 R1 的值放入寄存器 R0 所指向的地址 +40 处
0x005c 00092 (<unknown line number>) NOP
0x005c 00092 (main.go:9) STP (ZR, ZR), main..autotmp_11-16(SP)
0x0060 00096 (main.go:9) MOVD $3, R1
0x0064 00100 (main.go:9) MOVD R1, R2
0x0068 00104 (main.go:9) PCDATA $1, $1
0x0068 00104 (main.go:9) CALL runtime.convTslice(SB) ; 调用 convTslice 创建 slice,参数在 R0 中
0x006c 00108 (main.go:9) MOVD $type:[]interface {}(SB), R1
0x0074 00116 (main.go:9) MOVD R1, main..autotmp_11-16(SP)
0x0078 00120 (main.go:9) MOVD R0, main..autotmp_11-8(SP)
0x007c 00124 (main.go:9) PCDATA $0, $-3
0x007c 00124 (print.go:233) MOVD os.Stdout(SB), R1
从汇编代码中可以看出对第 6 行代码的处理细节,相同的是也创建了一个切片,不同的是切片内的元素为interface{}
类型,每个元素包含类型和数据部分
如果将第 6 行代码换成f([]int8{2, 5, 8}...)
,那么会创建一个[]int
类型的切片,而f(arr ...interface{})
所需的参数是[]interface{}
在创建[]int
和[]interface{}
时内存布局是不同的 (到此,终于形成闭环,理解了官网给的解释),所以不能把[]int
赋值给[]interface{}