变量存取
成员变量有两种读取方式,既可以通过对象读取,也可以通过对象的指针读取。两种读取方式与直接变量读取会有什么不同么?使用一段代码再看下:
type point struct { X float32 } type point2d struct { point Y float32 } type point3d struct { point2d Z float32 } func main() { var ( w float32 ) point := point3d{point2d: point2d{point: point{X: 1}, Y: 2}, Z: 3} // L25 p := &point // L26 w = point.Y // L27 fmt.Printf("w:%f\n", w) w = p.Y // L29 fmt.Printf("w:%f\n", w) }
还记得之前提过的“编译”阶段么?我们使用 go tool 可以查看源代码汇编之后的代码
data_access.go:25 0x10948d8 f30f11442444 MOVSS X0, 0x44(SP) data_access.go:25 0x10948de f30f11442448 MOVSS X0, 0x48(SP) data_access.go:25 0x10948e4 f30f1144244c MOVSS X0, 0x4c(SP) data_access.go:25 0x10948ea f30f10055ab50400 MOVSS $f32.3f800000(SB), X0 data_access.go:25 0x10948f2 f30f11442444 MOVSS X0, 0x44(SP) data_access.go:25 0x10948f8 f30f100550b50400 MOVSS $f32.40000000(SB), X0 data_access.go:25 0x1094900 f30f11442448 MOVSS X0, 0x48(SP) data_access.go:25 0x1094906 f30f100546b50400 MOVSS $f32.40400000(SB), X0 data_access.go:25 0x109490e f30f1144244c MOVSS X0, 0x4c(SP) data_access.go:26 0x1094914 488d442444 LEAQ 0x44(SP), AX data_access.go:26 0x1094919 4889442450 MOVQ AX, 0x50(SP) data_access.go:27 0x109491e f30f10442448 MOVSS 0x48(SP), X0 // 读取 Y 到寄存器 X0 data_access.go:27 0x1094924 f30f11442440 MOVSS X0, 0x40(SP) // 赋值 寄存器 X0 给 w ... data_access.go:29 0x10949c7 488b442450 MOVQ 0x50(SP), AX // 读取 对象地址 到寄存器 AX data_access.go:29 0x10949cc 8400 TESTB AL, 0(AX) data_access.go:29 0x10949ce f30f104004 MOVSS 0x4(AX), X0 // 从对象起始地址偏移4字节读取数据到寄存器 X0 data_access.go:29 0x10949d3 f30f11442440 MOVSS X0, 0x40(SP) // 赋值 寄存器 X0 给 w
可以看到,每个成员变量的偏移量在编译时即可获知,不管其有多么复杂的继承,都是一样的。通过对象存取一个data member,其效率和存取一个非成员变量是一样的。
函数调用
前面的例子提过,对象的总大小刚好等于所有的成员变量之和,也就意味着成员函数并不占用对象的内存大小。那成员函数的调用是怎么实现的呢?我们通过一段代码看下
type point3d struct { X, Y, Z float32 } func (p *point3d) Println() { fmt.Printf("%v,%v,%v\n", p.X, p.Y, p.Z) } func main() { p := point3d{X: 1, Y: 2, Z: 3} // L14 p.Println() // L15 }
同样使用 go tool获取对应的汇编代码
call.go:14 0x1094a7d 0f57c0 XORPS X0, X0 call.go:14 0x1094a80 f30f1144240c MOVSS X0, 0xc(SP) call.go:14 0x1094a86 f30f11442410 MOVSS X0, 0x10(SP) call.go:14 0x1094a8c f30f11442414 MOVSS X0, 0x14(SP) call.go:14 0x1094a92 f30f100592b30400 MOVSS $f32.3f800000(SB), X0 call.go:14 0x1094a9a f30f1144240c MOVSS X0, 0xc(SP) call.go:14 0x1094aa0 f30f100588b30400 MOVSS $f32.40000000(SB), X0 call.go:14 0x1094aa8 f30f11442410 MOVSS X0, 0x10(SP) call.go:14 0x1094aae f30f10057eb30400 MOVSS $f32.40400000(SB), X0 call.go:14 0x1094ab6 f30f11442414 MOVSS X0, 0x14(SP) call.go:15 0x1094abc 488d44240c LEAQ 0xc(SP), AX //将对象 q 的起始地址保存到寄存器AX call.go:15 0x1094ac1 48890424 MOVQ AX, 0(SP) //将对象 q 的起始地址 压栈 call.go:15 0x1094ac5 e8d6fdffff CALL main.(*point3d).Println(SB) // 调用 struct point 的 Println() 函数
可以看到成员函数的调用都是先把参数压栈,然后调用对应的的函数。可见,成员函数与普通的函数调用并无不同。那么函数的内存在哪里呢?
还记得进程的内存分布么?
没错,所有的函数都在进程的代码段(Text Segment)
Interface 语意学
第一部分讲了,封装和继承的影响,剩下这部分会讲清楚 Go 如何使用 interface 实现多态
和反射
。其中interface又有两种形式,一种是有函数的非空interface,一种是空的interface(interface{})。话不多说,直接上代码,看下这两种类型的interface的变量在内存大小上有何区别:
type Point interface { Println() } type point struct { X float32 } type point2d struct { point Y float32 } type point3d struct { point2d Z float32 } func TestPolymorphism(t *testing.T) { var ( p Point ) p = &point{X: 1} fmt.Printf("point size:%v\n\n", unsafe.Sizeof(p)) p = &point2d{point: point{X: 1}, Y: 2} fmt.Printf("point2d size:%v\n\n", unsafe.Sizeof(p)) p = &point3d{point2d: point2d{point: point{X: 1}, Y: 2}, Z: 3} fmt.Printf("point3d size:%v\n\n", unsafe.Sizeof(p)) }
执行程序输出为:
$ go test -v -run TestPolymorphism === RUN TestPolymorphism p size:16, nilP size:16 p size:16, nilP size:16 p size:16, nilP size:16
可以看到两种类型的interface 变量大小并无不同,均为16字节。可以明确一点:interface 变量中存储的并非对象的指针,而是特殊的定义类型的变量。那么 interface 是怎么支持多态
和反射
的呢?
通过 reflect
包,我们找到了答案。原来,针对以上两种类型的interface, Go 语言底层定义了两个结构分别为 iface 和 eface。两者实现是类似的,以下我们仅针对非空interface进行分析
interface 底层
type iface struct { tab *itab // 类型信息 data unsafe.Pointer // 接口指向对象的指针 } // 类型信息 type itab struct { inter *interfacetype // 接口的类型信息 _type *_type // 接口指向对象的类型信息 hash uint32 // copy of _type.hash. Used for type switches. _ [4]byte fun [1]uintptr // 接口方法实现列表,即函数地址列表,按字典序排序 } // 接口类型信息 type interfacetype struct { typ _type pkgpath name mhdr []imethod // 接口方法声明列表,按字典序排序 }
通过代码,可以看到,iface 类型包含两个指针,刚好为16字节(64位机器)。iface 不但包含了指向对象
、指向对象的类型
,还包含了接口类型
。如此
- iface 就可以在其中扮演粘结剂的角色,通过 reflect 包在对象、接口、类型之间进行转换了。
- iface 的变量可以在
编译
阶段,在变量赋值处,增加拷贝指向对象(父类或者子类)的类型信息的指令,就可以在运行期完成多态的支持了