來源:cyningsun.github.io/01-12-2020/…
源代碼:github.com/cyningsun/g…html
瞭解一門語言的高級特性,僅僅從浮於表面,是沒法把握住語言的精髓的。學習過 C++ 的高階開發者,必定讀過神書《Inside The C++ Object Model》,本文的目標是同樣的:經過對象模型,掌握 Go 語言的底層機制,從更深層次解釋語言特性。git
衆所周知,Go 源碼並不能直接運行,全部代碼必須一行行,經過「編譯」——「彙編」——「連接」 階段 轉化爲低級的機器語言指令,便可執行程序。程序員
「彙編」和「連接」階段各類語言並沒有區別,因此通常經過「編譯」和「執行」階段來支持各類語言特性。對於 Go 語言,執行過程並沒有法直接修改執行指令,所以全部語言特性都是「編譯」相關的。理解這一點很重要,由於下面依賴「編譯」的產物 彙編代碼 來解讀對象模型。github
何爲 Go 對象模型? Go 對象模型能夠歸納爲如下兩部分:golang
支持面向對象程序設計的部分編程
- 封裝
- 繼承
- 多態
各類特性的底層實現機制bash
- 反射
下面分別從 struct 和 interface 來解釋模型如何支持以上兩部分。ide
面向對象編程,把對象做爲程序的基本單元,一個對象包含了數據和操做數據的函數,前者爲成員變量,後者爲成員函數。因此研究對象須要分別從成員變量和成員函數入手。函數
如下有三段程序:佈局
// First: global varible
var (
X,Y,Z float32
)
// Second: simple type
type point3d struct {
X, Y, Z float32
}
// Third: inherit type
type point struct {
X float32
}
type point2d struct {
point
Y float32
}
type point3d struct {
point2d
Z float32
}
複製代碼
從風格來看,三段程序大相徑庭。有許多使人信服的討論告訴咱們,爲何「數據封裝」(Second & Third)要比使用「全局變量」好。但,從程序員的角度看,會有幾個疑問:
- 「數據封裝」 以後,內存成本增長了多少?
- 「數據封裝」 以後,在執行過程當中,變量的存儲效率是否下降了?
先看內存變化。瞭解內存變化最好的辦法就是經過代碼打印對象的內存大小,先看全局變量大小
var (
X, Y, Z float32
)
func main() {
fmt.Printf("X size:%v, Y size:%v, Z size:%v\n", unsafe.Sizeof(X), unsafe.Sizeof(Y), unsafe.Sizeof(Z))
fmt.Printf("X addr:%v, Y addr:%v, Z addr:%v\n", &X, &Y, &Z)
}
複製代碼
執行程序輸出爲:
$ go run variable.go
X size:4, Y size:4, Z size:4
X addr:0x118ee88, Y addr:0x118ee8c, Z addr:0x118ee90
複製代碼
能夠看到,X、Y、Z三個字段大小均爲4字節,且三個字段內存地址順序排列。
再看第二段代碼的輸出
func TestLayout(t *testing.T) {
p := point3d{X: 1, Y: 2, Z: 3}
fmt.Printf("point3d size:%v, align:%v\n", unsafe.Sizeof(p), unsafe.Alignof(p))
typ := reflect.TypeOf(p)
fmt.Printf("Struct:%v is %d bytes long\n", typ.Name(), typ.Size())
fmt.Printf("X at offset %v, size=%d\n", unsafe.Offsetof(p.X), unsafe.Sizeof(p.X))
fmt.Printf("Y at offset %v, size=%d\n", unsafe.Offsetof(p.Y), unsafe.Sizeof(p.Y))
fmt.Printf("Z at offset %v, size=%d\n", unsafe.Offsetof(p.Z), unsafe.Sizeof(p.Z))
}
複製代碼
執行程序輸出爲:
$ go test -v -run TestLayout
=== RUN TestLayout
point3d size:12, align:4
Struct:point3d is 12 bytes long
X at offset 0, size=4
Y at offset 4, size=4
Z at offset 8, size=4
複製代碼
能夠看到,X、Y、Z三個字段大小同樣爲4字節,內存排列也與上一個版本同樣。
繼續,第三段代碼
func TestLayout(t *testing.T) {
p := point3d{point2d: point2d{point: point{X: 1}, Y: 2}, Z: 3}
fmt.Printf("point3d size:%v, align:%v\n", unsafe.Sizeof(p), unsafe.Alignof(p))
typ := reflect.TypeOf(p)
fmt.Printf("Struct:%v is %d bytes long\n", typ.Name(), typ.Size())
fmt.Printf("X at offset %v, size=%d\n", unsafe.Offsetof(p.X), unsafe.Sizeof(p.X))
fmt.Printf("Y at offset %v, size=%d\n", unsafe.Offsetof(p.Y), unsafe.Sizeof(p.Y))
fmt.Printf("Z at offset %v, size=%d\n", unsafe.Offsetof(p.Z), unsafe.Sizeof(p.Z))
}
複製代碼
執行程序輸出爲:
$ go test -v -run TestLayout
=== RUN TestLayout
point3d size:12, align:4
Struct:point3d is 12 bytes long
X at offset 0, size=4
Y at offset 4, size=4
Z at offset 8, size=4
複製代碼
能夠看到,X、Y、Z三個字段大小同樣爲4字節,內存排列也與以前兩個版本同樣。
綜上所述,咱們能夠看到,不管是否封裝,仍是多深的繼承層次,對成員變量的內存佈局都並沒有影響,均按照字段定義的順序排列(不考慮內存對齊的狀況)。即內存佈局相似以下:
成員變量有兩種讀取方式,既能夠經過對象讀取,也能夠經過對象的指針讀取。兩種讀取方式與直接變量讀取會有什麼不一樣麼?使用一段代碼再看下:
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)
第一部分講了,封裝和繼承的影響,剩下這部分會講清楚 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進行分析
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 不但包含了指向對象
、指向對象的類型
,還包含了接口類型
。如此
編譯
階段,在變量賦值處,增長拷貝指向對象(父類或者子類)的類型信息的指令,就能夠在運行期完成多態的支持了下面咱們仍是經過測試代碼來驗證咱們的理論,咱們本身定義底層的相關類型,而後經過強制類型轉換,來嘗試解析interface變量中的數據:
type Iface struct {
Tab *Itab
Data unsafe.Pointer
}
type Itab struct {
Inter uintptr
Type uintptr
Hash uint32
_ [4]byte
Fun [1]uintptr
}
type Eface struct {
Type uintptr
Data unsafe.Pointer
}
func TestInterface(t *testing.T) {
var (
p Point
nilP interface{}
)
point := &point3d{X: 1, Y: 2, Z: 3}
nilP = point
fmt.Printf("eface size:%v\n", unsafe.Sizeof(nilP))
eface := (*face.Eface)(unsafe.Pointer(&nilP))
spew.Dump(eface.Type)
spew.Dump(eface.Data)
fmt.Printf("eface offset: eface._type = %v, eface.data = %v\n\n",
unsafe.Offsetof(eface.Type), unsafe.Offsetof(eface.Data))
p = point
fmt.Printf("point size:%v\n", unsafe.Sizeof(p))
iface := (*face.Iface)(unsafe.Pointer(&p))
spew.Dump(iface.Tab)
spew.Dump(iface.Data)
fmt.Printf("Iface offset: iface.tab = %v, iface.data = %v\n\n",
unsafe.Offsetof(iface.Tab), unsafe.Offsetof(iface.Data))
}
複製代碼
執行程序輸出爲:
$ go test -v -run TestInterface
=== RUN TestInterface
eface size:16
(uintptr) 0x111f2c0
(unsafe.Pointer) 0xc00008e250
eface offset: eface._type = 0, eface.data = 8
point size:16
(*face.Itab)(0x116ec40)({
Inter: (uintptr) 0x1122680,
Type: (uintptr) 0x111f2c0,
Hash: (uint32) 960374823,
_: ([4]uint8) (len=4 cap=4) {
00000000 00 00 00 00 |....|
},
Fun: ([1]uintptr) (len=1 cap=1) {
(uintptr) 0x10fce20
}
})
(unsafe.Pointer) 0xc00008e250
Iface offset: iface.tab = 0, iface.data = 8
複製代碼
下面咱們再經過彙編代碼看下,賦值操做作了什麼?
type Point interface {
Println()
}
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() {
point := point3d{X: 1, Y: 2, Z: 3} // L18
var (
nilP interface{} // L20
p Point // L21
)
nilP = &point // L23
p = &point // L24
fmt.Println(nilP, p)
}
複製代碼
經過 go tool 查看彙編代碼以下:
TEXT main.main(SB) /Users/cyningsun/Documents/go/src/github.com/cyningsun/go-test/20200102-inside-golang-object-model/main/build.go
build.go:17 0x1094de0 65488b0c2530000000 MOVQ GS:0x30, CX
build.go:17 0x1094de9 488d4424b0 LEAQ -0x50(SP), AX
build.go:17 0x1094dee 483b4110 CMPQ 0x10(CX), AX
build.go:17 0x1094df2 0f86b9010000 JBE 0x1094fb1
build.go:17 0x1094df8 4881ecd0000000 SUBQ $0xd0, SP
build.go:17 0x1094dff 4889ac24c8000000 MOVQ BP, 0xc8(SP)
build.go:17 0x1094e07 488dac24c8000000 LEAQ 0xc8(SP), BP
build.go:18 0x1094e0f 488d05ea1e0200 LEAQ type.*+137216(SB), AX // point := point3d{X: 1, Y: 2, Z: 3}
build.go:18 0x1094e16 48890424 MOVQ AX, 0(SP)
build.go:18 0x1094e1a e81160f7ff CALL runtime.newobject(SB)
build.go:18 0x1094e1f 488b442408 MOVQ 0x8(SP), AX
build.go:18 0x1094e24 4889442458 MOVQ AX, 0x58(SP)
build.go:18 0x1094e29 0f57c0 XORPS X0, X0
build.go:18 0x1094e2c f30f11442434 MOVSS X0, 0x34(SP)
build.go:18 0x1094e32 f30f11442438 MOVSS X0, 0x38(SP)
build.go:18 0x1094e38 f30f1144243c MOVSS X0, 0x3c(SP)
build.go:18 0x1094e3e f30f1005a6b80400 MOVSS $f32.3f800000(SB), X0
build.go:18 0x1094e46 f30f11442434 MOVSS X0, 0x34(SP)
build.go:18 0x1094e4c f30f100d9cb80400 MOVSS $f32.40000000(SB), X1
build.go:18 0x1094e54 f30f114c2438 MOVSS X1, 0x38(SP)
build.go:18 0x1094e5a f30f101592b80400 MOVSS $f32.40400000(SB), X2
build.go:18 0x1094e62 f30f1154243c MOVSS X2, 0x3c(SP)
build.go:18 0x1094e68 488b442458 MOVQ 0x58(SP), AX
build.go:18 0x1094e6d f30f1100 MOVSS X0, 0(AX)
build.go:18 0x1094e71 f30f114804 MOVSS X1, 0x4(AX)
build.go:18 0x1094e76 f30f115008 MOVSS X2, 0x8(AX)
build.go:20 0x1094e7b 0f57c0 XORPS X0, X0 // nilP interface{}
build.go:20 0x1094e7e 0f11442470 MOVUPS X0, 0x70(SP)// nilP 開始地址爲0x70
build.go:21 0x1094e83 0f57c0 XORPS X0, X0 // p Point
build.go:21 0x1094e86 0f11442460 MOVUPS X0, 0x60(SP)
build.go:23 0x1094e8b 488b442458 MOVQ 0x58(SP), AX // nilP = &point ;0x58(SP) 爲 point 的地址
build.go:23 0x1094e90 4889442448 MOVQ AX, 0x48(SP) // SP 指向 point 地址
build.go:23 0x1094e95 488d0da4860100 LEAQ type.*+98368(SB), CX // ;從內存加載 Point類型地址 到 CX 寄存器
build.go:23 0x1094e9c 48894c2470 MOVQ CX, 0x70(SP) // ;將 Point類型地址(8字節) 保存到 0x70(即eface._type)
build.go:23 0x1094ea1 4889442478 MOVQ AX, 0x78(SP) // ;將 point 對象地址(8字節) 保存到 0x78(即eface.data)
build.go:24 0x1094ea6 488b442458 MOVQ 0x58(SP), AX // p = &point
build.go:24 0x1094eab 4889442448 MOVQ AX, 0x48(SP) // ;SP 指向 point 地址
build.go:24 0x1094eb0 488d0d09d50400 LEAQ go.itab.*main.point3d,main.Point(SB), CX // ;從內存加載 Point類型 itab 地址 到 CX 寄存器
build.go:24 0x1094eb7 48894c2460 MOVQ CX, 0x60(SP) // ;將 Point類型地址(8字節) 保存到 0x70(即iface.tab)
build.go:24 0x1094ebc 4889442468 MOVQ AX, 0x68(SP) // ;將 point 對象地址(8字節) 保存到 0x78(即iface.data)
build.go:25 0x1094ec1 488b442468 MOVQ 0x68(SP), AX // fmt.Println(nilP, p)
...
複製代碼
事實正如理論通常,在編譯
階段,賦值命令被轉化爲類型信息和對象指針的拷貝,保存下來執行期轉換所須要的一切信息。
從底層代碼和彙編出發,分析 struct 和 interface 的 對象模型,理清了Go 語言高級特性的底層機制。再去學習反射等表層細節,事半功倍。
參考連接: