深度探索 Go 对象模型(二)

简介: 深度探索 Go 对象模型(二)
变量存取

成员变量有两种读取方式,既可以通过对象读取,也可以通过对象的指针读取。两种读取方式与直接变量读取会有什么不同么?使用一段代码再看下:

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 不但包含了指向对象指向对象的类型,还包含了接口类型。如此

  1. iface 就可以在其中扮演粘结剂的角色,通过 reflect 包在对象、接口、类型之间进行转换了。
  2. iface 的变量可以在编译阶段,在变量赋值处,增加拷贝指向对象(父类或者子类)的类型信息的指令,就可以在运行期完成多态的支持了


目录
相关文章
|
17天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
1月前
|
存储 安全 Java
go语言重用对象
【10月更文挑战第19天】
29 1
|
1月前
|
Go 调度 开发者
Go语言的并发编程模型
【10月更文挑战第26天】Go语言的并发编程模型
16 1
|
1月前
|
安全 测试技术 Go
Go语言中的并发编程模型解析####
在当今的软件开发领域,高效的并发处理能力是提升系统性能的关键。本文深入探讨了Go语言独特的并发编程模型——goroutines和channels,通过实例解析其工作原理、优势及最佳实践,旨在为开发者提供实用的Go语言并发编程指南。 ####
|
2月前
|
负载均衡 安全 物联网
探索Go语言的并发编程模型及其在现代应用中的优势
【10月更文挑战第10天】探索Go语言的并发编程模型及其在现代应用中的优势
|
5月前
|
缓存 编译器 Go
开发与运维线程问题之Go语言的goroutine基于线程模型实现如何解决
开发与运维线程问题之Go语言的goroutine基于线程模型实现如何解决
60 3
|
6月前
|
Go 开发者
探索Go语言的并发编程模型
通过实例详细介绍了Go语言中的并发编程模型,包括goroutine、channel的基本使用和最佳实践。深入剖析如何利用Go的并发特性提高程序性能和效率,适用于初学者和有一定经验的开发者。
|
7月前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第2天】Go语言的并发编程基于CSP模型,强调通过通信共享内存。核心概念是goroutines(轻量级线程)和channels(用于goroutines间安全数据传输)。常见问题包括数据竞争、死锁和goroutine管理。避免策略包括使用同步原语、复用channel和控制并发。示例展示了如何使用channel和`sync.WaitGroup`避免死锁。理解并发原则和正确应用CSP模型是编写高效安全并发程序的关键。
178 7
|
7月前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
165 2
|
7月前
|
数据可视化
R语言多元(多变量)GARCH :GO-GARCH、BEKK、DCC-GARCH和CCC-GARCH模型和可视化
R语言多元(多变量)GARCH :GO-GARCH、BEKK、DCC-GARCH和CCC-GARCH模型和可视化