在 Go 中...是一种语法糖,极大程度的方便程序的书写,下面先介绍几种常见用法!!
xxxxxxxxxx// 定义数组 (切记是数组,并非切片)// 下面两种方式等价,变量 a,b 均为大小为 3 的数组,即:[3]inta := [...]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 中继承没有多态,也就是无法将子类对象赋值给父类
xxxxxxxxxxtype 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{} // 定义一个空接口变量 a1var a2 any // 定义一个空接口变量 a2秉着不到底层不死心的原则,下面再来看看空接口的底层结构:
xxxxxxxxxx// src/runtime/runtime2.gotype eface struct { _type *_type // 类型指针 data unsafe.Pointer // 数据指针}下面给一个小例子:
xxxxxxxxxxfunc 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的值
介绍完了...语法糖和万能类型,下面引出这两个结合在一起带来的一个坑。先看一个例子:
xxxxxxxxxxfunc 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,所以类型不匹配,直接报错!!
下面再给一个例子:
xxxxxxxxxxfunc 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, $00x0020 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>) NOP0x003c 00060 (main.go:9) STP (ZR, ZR), main..autotmp_11-16(SP)0x0040 00064 (main.go:9) MOVD $3, R10x0044 00068 (main.go:9) MOVD R1, R20x0048 00072 (main.go:9) PCDATA $1, $10x0048 00072 (main.go:9) CALL runtime.convTslice(SB) ; 调用 convTslice 创建 slice,参数在 R0 中0x004c 00076 (main.go:9) MOVD $type:[]int(SB), R10x0054 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, $-30x005c 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, $00x0020 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>) NOP0x005c 00092 (main.go:9) STP (ZR, ZR), main..autotmp_11-16(SP)0x0060 00096 (main.go:9) MOVD $3, R10x0064 00100 (main.go:9) MOVD R1, R20x0068 00104 (main.go:9) PCDATA $1, $10x0068 00104 (main.go:9) CALL runtime.convTslice(SB) ; 调用 convTslice 创建 slice,参数在 R0 中0x006c 00108 (main.go:9) MOVD $type:[]interface {}(SB), R10x0074 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, $-30x007c 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{}