接口的构造过程是怎样的
-
我们已经看过了
iface
和eface
的源码- 知道
iface
最重要的是itab
和_type
- 知道
-
为了研究清楚接口是如何构造的
- 接下来我会拿起汇编的武器
- 还原背后的真相
-
来看一个示例代码:
package main
import "fmt"
type Person interface {
growUp()
}
type Student struct {
age int
}
func (p Student) growUp() {
p.age += 1
return
}
func main() {
var didi = Person(Student{age: 18})
fmt.Println(didi)
}
// 执行命令:
go tool compile -S ./src/main.go
// 得到 main 函数的汇编代码如下:
0x0000 00000 (./src/main.go:30) TEXT "".main(SB), $80-0
0x0000 00000 (./src/main.go:30) MOVQ (TLS), CX
0x0009 00009 (./src/main.go:30) CMPQ SP, 16(CX)
0x000d 00013 (./src/main.go:30) JLS 157
0x0013 00019 (./src/main.go:30) SUBQ $80, SP
0x0017 00023 (./src/main.go:30) MOVQ BP, 72(SP)
0x001c 00028 (./src/main.go:30) LEAQ 72(SP), BP
0x0021 00033 (./src/main.go:30) FUNCDATA$0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (./src/main.go:30) FUNCDATA$1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB)
0x0021 00033 (./src/main.go:31) MOVQ $18, ""..autotmp_1+48(SP)
0x002a 00042 (./src/main.go:31) LEAQ go.itab."".Student,"".Person(SB), AX
0x0031 00049 (./src/main.go:31) MOVQ AX, (SP)
0x0035 00053 (./src/main.go:31) LEAQ ""..autotmp_1+48(SP), AX
0x003a 00058 (./src/main.go:31) MOVQ AX, 8(SP)
0x003f 00063 (./src/main.go:31) PCDATA $0, $0
0x003f 00063 (./src/main.go:31) CALL runtime.convT2I64(SB)
0x0044 00068 (./src/main.go:31) MOVQ 24(SP), AX
0x0049 00073 (./src/main.go:31) MOVQ 16(SP), CX
0x004e 00078 (./src/main.go:33) TESTQ CX, CX
0x0051 00081 (./src/main.go:33) JEQ 87
0x0053 00083 (./src/main.go:33) MOVQ 8(CX), CX
0x0057 00087 (./src/main.go:33) MOVQ $0, ""..autotmp_2+56(SP)
0x0060 00096 (./src/main.go:33) MOVQ $0, ""..autotmp_2+64(SP)
0x0069 00105 (./src/main.go:33) MOVQ CX, ""..autotmp_2+56(SP)
0x006e 00110 (./src/main.go:33) MOVQ AX, ""..autotmp_2+64(SP)
0x0073 00115 (./src/main.go:33) LEAQ ""..autotmp_2+56(SP), AX
0x0078 00120 (./src/main.go:33) MOVQ AX, (SP)
0x007c 00124 (./src/main.go:33) MOVQ $1, 8(SP)
0x0085 00133 (./src/main.go:33) MOVQ $1, 16(SP)
0x008e 00142 (./src/main.go:33) PCDATA $0, $1
0x008e 00142 (./src/main.go:33) CALL fmt.Println(SB)
0x0093 00147 (./src/main.go:34) MOVQ 72(SP), BP
0x0098 00152 (./src/main.go:34) ADDQ $80, SP
0x009c 00156 (./src/main.go:34) RET
0x009d 00157 (./src/main.go:34) NOP
0x009d 00157 (./src/main.go:30) PCDATA $0, $-1
0x009d 00157 (./src/main.go:30) CALL runtime.morestack_noctxt(SB)
0x00a2 00162 (./src/main.go:30) JMP 0
- 我们从第 10 行开始看
汇编行数 | 操作 |
---|---|
10-14 | 构造调用 runtime.convT2I64(SB) 的参数 |
们来看下这个函数的参数形式:
func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
// ……
}
类型转换和断言的区别
-
我们知道,Go 语言中不允许隐式类型转换
-
也就是说
=
两边,不允许出现类型不相同的变量 -
类型转换
、类型断言
本质都是把一个类型转换成另外一个类型 -
不同之处在于
- 类型断言是对接口变量进行的操作
类型转换
- 对于
类型转换
而言,转换前后的两个类型要相互兼容才行- 类型转换的语法为:
<结果类型> := <目标类型> (<表达式>)
package main
import "fmt"
func main() {
var i int = 9
var f float64
f = float64(i)
fmt.Printf("%T, %v\n", f, f)
f = 10.8
a := int(f)
fmt.Printf("%T, %v\n", a, a)
// s := []int(i)
-
上面的代码里,我定义了一个
int
型和float64
型的变量 -
尝试在它们之前相互转换
- 结果是成功的:
int
型和float64
是相互兼容的
- 结果是成功的:
-
如果我把最后一行代码的注释去掉
-
编译器会报告类型不兼容的错误
断言
-
前面说过,因为空接口
interface{}
没有定义任何函数 -
因此 Go 中所有类型都实现了空接口
-
当一个函数的形参是
interface{}
- 那么在函数中
- 需要对形参进行断言
- 从而得到它的真实类型
-
断言的语法为:
// 安全类型断言
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )
//非安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )
-
类型转换和类型断言有些相似
-
不同之处在于类型断言是对接口进行的操作
还是来看一个简短的例子:
package main
import "fmt"
type Student struct {
Name string
Age int
}
func main() {
var i interface{} = new(Student)
s := i.(Student)
fmt.Println(s)
}
// 运行一下:
panic: interface conversion: interface {} is *main.Student, not main.Student
- 直接
panic
了 - 这是因为
i
- 是
*Student
类型 - 并非
Student
类型
- 是
- 断言失败
- 这里直接发生了
panic
- 这里直接发生了
- 线上代码可能并不适合这样做
- 可以采用“安全断言”的语法:
func main() {
var i interface{} = new(Student)
s, ok := i.(Student)
if ok {
fmt.Println(s)
}
}
-
这样,即使断言失败也不会
panic
-
断言其实还有另一种形式
-
就是利用
switch
语句判断接口的类型- 每一个
case
会被顺序地考虑 - 当命中一个
case
时- 就会执行
case
中的语句
- 就会执行
- 每一个
-
因此
case
语句的顺序是很重要的- 因为很有可能会有多个
case
匹配的情况
- 因为很有可能会有多个
代码示例如下:
func main() {
//var i interface{} = new(Student)
//var i interface{} = (*Student)(nil)
var i interface{}
fmt.Printf("%p %v\n", &i, i)
judge(i)
}
func judge(v interface{}) {
fmt.Printf("%p %v\n", &v, v)
switch v := v.(type) {
case nil:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("nil type[%T] %v\n", v, v)
case Student:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("Student type[%T] %v\n", v, v)
case *Student:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("*Student type[%T] %v\n", v, v)
default:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("unknow\n")
}
}
type Student struct {
Name string
Age int
}
// main 函数里有三行不同的声明,每次运行一行,注释另外两行,得到三组运行结果:
// --- var i interface{} = new(Student)
0xc4200701b0 [Name: ], [Age: 0]
0xc4200701d0 [Name: ], [Age: 0]
0xc420080020 [Name: ], [Age: 0]
*Student type[*main.Student] [Name: ], [Age: 0]
// --- var i interface{} = (*Student)(nil)
0xc42000e1d0 <nil>
0xc42000e1f0 <nil>
0xc42000c030 <nil>
*Student type[*main.Student] <nil>
// --- var i interface{}
0xc42000e1d0 <nil>
0xc42000e1e0 <nil>
0xc42000e1f0 <nil>
nil type[<nil>] <nil>
对于第一行语句:
var i interface{} = new(Student)
i
是一个*Student
类型- 匹配上第三个 case
- 从打印的三个地址来看
- 这三处的变量实际上都是不一样的
- 在
main
函数里有一个局部变量i
- 调用函数时
- 实际上是复制了一份参数
- 因此函数里又有一个变量
v
,它是i
的拷贝;断言之后,又生成了一份新的拷贝。所以最终打印的三个变量的地址都不一样。
对于第二行语句:
var i interface{} = (*Student)(nil)
这里想说明的其实是 i
在这里动态类型是 (*Student)
, 数据为 nil
,它的类型并不是 nil
,它与 nil
作比较的时候,得到的结果也是 false
。
最后一行语句:
var i interface{}
这回 i
才是 nil
类型。
【引申1】 fmt.Println
函数的参数是 interface
。对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 String()
方法,如果实现了,则直接打印输出 String()
方法的结果;否则,会通过反射来遍历对象的成员进行打印。