PHP与go接口,原来go的接口内部实现是这样的

1 前言

1.1 Go汇编

Go语言被定义为一门系统编程语言,与C语言一样通过编译器生成可直接运行的二进制文件。这一点与Java,PHP,Python等编程语言存在很大的不同,这些语言都是运行在基于C语言开发的虚拟机上,如果想深入了解运行原理只需要看懂对应的C语言开发的虚拟机(绝大部分程序员应该都对C语言有基本的了解)。但是如果想深入学习Go语言,就需要对基本的汇编指令和语法有一定的了解(通过汇编可以了解到编译器到底做了什么工作)。

通过下面的例子简单了解如何通过汇编来了解Go语言的运行原理。编辑一个go文本call_function.go,输入如下代码:

1 package main

2

3 func add(a, b int) int {

4 return a + b

5 }

6

7 func main() {

8 a := 10

9 b := 20

10

11 c := add(a, b)

12 _ = c

13 }

输入命令go build -gcflags '-l -N' call_function.go生成可执行文件,然后输入命令go tool objdump -s "main.main" call_function查看汇编代码如下:

1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/call_function.go

2 call_function.go:7 0x104f380 65488b0c25a0080000 MOVQ GS:0x8a0, CX

3 call_function.go:7 0x104f389 483b6110 CMPQ 0x10(CX), SP

4 call_function.go:7 0x104f38d 764c JBE 0x104f3db

5 call_function.go:7 0x104f38f 4883ec38 SUBQ $0x38, SP

6 call_function.go:7 0x104f393 48896c2430 MOVQ BP, 0x30(SP)

7 call_function.go:7 0x104f398 488d6c2430 LEAQ 0x30(SP), BP

8 call_function.go:8 0x104f39d 48c74424280a000000 MOVQ $0xa, 0x28(SP)

9 call_function.go:9 0x104f3a6 48c744242014000000 MOVQ $0x14, 0x20(SP)

10 call_function.go:11 0x104f3af 488b442428 MOVQ 0x28(SP), AX

11 call_function.go:11 0x104f3b4 48890424 MOVQ AX, 0(SP)

12 call_function.go:11 0x104f3b8 488b442420 MOVQ 0x20(SP), AX

13 call_function.go:11 0x104f3bd 4889442408 MOVQ AX, 0x8(SP)

14 call_function.go:11 0x104f3c2 e899ffffff CALL main.add(SB)

15 call_function.go:11 0x104f3c7 488b442410 MOVQ 0x10(SP), AX

16 call_function.go:11 0x104f3cc 4889442418 MOVQ AX, 0x18(SP)

17 call_function.go:13 0x104f3d1 488b6c2430 MOVQ 0x30(SP), BP

18 call_function.go:13 0x104f3d6 4883c438 ADDQ $0x38, SP

19 call_function.go:13 0x104f3da c3 RET

20 call_function.go:7 0x104f3db e89083ffff CALL runtime.morestack_noctxt(SB)

21 call_function.go:7 0x104f3e0 eb9e JMP main.main(SB)

第8~9行汇编代码,分别将SP(栈寄存器)偏移0x28和0x20的地址赋值为0xa和0x14,对应Go代码的第8行和第9行中的对a,b变量赋值,也就是说a变量对应的内存地址是SP+0x28,b变量对应的内存地址是SP+0x20。

然后10~14行汇编代码表示对a,b变量进行拷贝,分别拷贝到SP+0x0和SP+0x8地址,然后调用add方法,这就是通常说到的函数调用时的“值传递”。

输入命令go tool objdump -s "main.add" call_function,可以看到如下的汇编代码:

1 TEXT main.add(SB) /Users/didi/Source/Go/src/ppt/call_function.go

2 call_function.go:3 0x104f360 48c744241800000000 MOVQ $0x0, 0x18(SP)

3 call_function.go:4 0x104f369 488b442408 MOVQ 0x8(SP), AX

4 call_function.go:4 0x104f36e 4803442410 ADDQ 0x10(SP), AX

5 call_function.go:4 0x104f373 4889442418 MOVQ AX, 0x18(SP)

6 call_function.go:4 0x104f378 c3 RET

第3~5行汇编代码表示,将SP+0x8和SP+0x10地址的值相加,并复制到SP+0x18地址。

为什么在main函数中,a和b变量分别复制到了SP+0x0和SP+0x8地址,但是在add函数中,却将SP+0x8和SP+0x10地址的值进行相加呢?

这是因为在main函数中的汇编代码14行中,调用call执行时CPU会执行一次压栈操作,将函数调用完成以后需要返回的地址存在SP-0x8的地址处,并执行一次SP=SP-0x8的操作(具体操作可以百度一下)。所以在add函数里面的SP+0x8和SP+0x10地址就对应着main函数中的SP+0x0和SP+0x8地址。

具体过程如下图:

db9b0cebde85

image

1.2 Go指针

Go的库代码中大量使用了一些指针进行内存操作。但是在Go语言中指针变量是不能进行运算的,所以不能像C语言那样方便的对内存进行偏移寻址,但是Go中提供了unsafe包来对指针计算运算。

下面的例子可以说明使用方式:

1 package main

2

3 import (

4 "fmt"

5 "unsafe"

6 )

7

8 type Struct1 struct {

9 A int64

10 B int64

11 C int64

12 }

13

14 type Struct2 struct {

15 A int64

16 B int64

17 C int64

18 }

19

20 func main() {

21 struct1 := Struct1 {

22 A : 1,

23 B : 2,

24 C : 3,

25 }

26

27 struct2 := new(Struct2)

28

29 var src uintptr = uintptr(unsafe.Pointer(&struct1))

30 var dst uintptr = uintptr(unsafe.Pointer(struct2))

31 for i := 0; i < 24; i++ {

32 *(*uint8)(unsafe.Pointer(dst + uintptr(i))) = *(*uint8)(unsafe.Pointer(src + uintptr(i)))

33 }

34

35 fmt.Println("struct1=%v||struct2=%v", struct1, *struct2);

36 }

在上面的例子将struct1对应内存的值复制到struct2对应的内存中,从例子中可以看出可以看到Go语言中

unsafe.Pointer类似于C中的void*,任何类型的指针都可以转换为unsafe.Pointer 类型,unsafe.Pointer 类型也可以转换为任何指针类型;

uintptr可以存go中的任何变量,如果想对指针进行运算,必须先把指针转换为uintptr。

2 Go的interface的实现

在Go语言中interface是一个非常重要的概念,也是与其它语言相比存在很大特色的地方。interface也是一个Go语言中的一种类型,是一种比较特殊的类型,存在两种interface,一种是带有方法的interface,一种是不带方法的interface。Go语言中的所有变量都可以赋值给空interface变量,实现了interface中定义方法的变量可以赋值给带方法的interface变量,并且可以通过interface直接调用对应的方法,实现了其它面向对象语言的多态的概念。

2.1 内部定义

两种不同的interface在Go语言内部被定义成如下的两种结构体(源码基于Go的1.9.2版本):

// 没有方法的interface

type eface struct {

_type *_type

data unsafe.Pointer

}

// 记录着Go语言中某个数据类型的基本特征

type _type struct {

size uintptr

ptrdata uintptr

hash uint32

tflag tflag

align uint8

fieldalign uint8

kind uint8

alg *typeAlg

gcdata *byte

str nameOff

ptrToThis typeOff

}

// 有方法的interface

type iface struct {

tab *itab

data unsafe.Pointer

}

type itab struct {

inter *interfacetype

_type *_type

link *itab

hash uint32

bad bool

inhash bool

unused [2]byte

fun [1]uintptr

}

// interface数据类型对应的type

type interfacetype struct {

typ _type

pkgpath name

mhdr []imethod

}

可以看到两种类型的interface在内部实现时都是定义成了一个2个字段的结构体,所以任何一个interface变量都是占用16个byte的内存空间。

在Go语言中_type这个结构体非常重要,记录着某种数据类型的一些基本特征,比如这个数据类型占用的内存大小(size字段),数据类型的名称(nameOff字段)等等。每种数据类型都存在一个与之对应的_type结构体(Go语言原生的各种数据类型,用户自定义的结构体,用户自定义的interface等等)。如果是一些比较特殊的数据类型,可能还会对_type结构体进行扩展,记录更多的信息,比如interface类型,就会存在一个interfacetype结构体,除了通用的_type外,还包含了另外两个字段pkgpath和mhdr,后文在对这两个字段的作用进行解析。除此之外还有其它类型的数据结构对应的结构体,比如structtype,chantype,slicetype,有兴趣的可以在$GOROOT/src/runtime/type.go文件中查看。

db9b0cebde85

image

2.2 赋值

存在对没有方法的interface变量和有方法的interface变量赋值这两种不同的情况。分别详解这两种不同的赋值过程。

没有方法的interface变量赋值

对没有方法的interface变量赋值时编译器做了什么工作?创建一个eface.go文件,代码如下:

1 package main

2

3 type Struct1 struct {

4 A int64

5 B int64

6 }

7

8 func main() {

9 s := new(Struct1)

10 var i interface{}

11 i = a

12

13 _ = i

14 }

输入命令go build -gcflags '-l -N' eface.go,go tool objdump -s "main.main" eface,查看汇编代码。

1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/eface.go

2 eface.go:8 0x104f360 4883ec38 SUBQ $0x38, SP

3 eface.go:8 0x104f364 48896c2430 MOVQ BP, 0x30(SP)

4 eface.go:8 0x104f369 488d6c2430 LEAQ 0x30(SP), BP

5 eface.go:9 0x104f36e 48c7042400000000 MOVQ $0x0, 0(SP)

6 eface.go:9 0x104f376 48c744240800000000 MOVQ $0x0, 0x8(SP)

7 eface.go:9 0x104f37f 488d0424 LEAQ 0(SP), AX

8 eface.go:9 0x104f383 4889442410 MOVQ AX, 0x10(SP)

9 eface.go:10 0x104f388 48c744242000000000 MOVQ $0x0, 0x20(SP)

10 eface.go:10 0x104f391 48c744242800000000 MOVQ $0x0, 0x28(SP)

11 eface.go:11 0x104f39a 488b442410 MOVQ 0x10(SP), AX

12 eface.go:11 0x104f39f 4889442418 MOVQ AX, 0x18(SP)

13 eface.go:11 0x104f3a4 488d0dd5670000 LEAQ 0x67d5(IP), CX

14 eface.go:11 0x104f3ab 48894c2420 MOVQ CX, 0x20(SP)

15 eface.go:11 0x104f3b0 4889442428 MOVQ AX, 0x28(SP)

16 eface.go:14 0x104f3b5 488b6c2430 MOVQ 0x30(SP), BP

17 eface.go:14 0x104f3ba 4883c438 ADDQ $0x38, SP

汇编代码第56行给结构体Struct1分配了空间SP+0x0和SP+0x8,第78行把这个结构体的地址放在存入了SP+0x10地址,这个地址就是变量s,第910行给interface类型的变量i分配了SP+0x20和SP+0x28,第1314行把结构体A对应的_type的地址赋值到SP+0x20,然后把a变量赋值到了SP+0x28。这就是对没有方法的interface进行赋值的过程。赋值完以后的内存分配如下图:

db9b0cebde85

image

有方法的interface变量赋值

如下一段代码在内存的分布

1 package main

2

3 type I interface {

4 Add()

5 Del()

6 }

7

8 type Struct1 struct {

9 A int64

10 B int64

11 }

12

13 func (a *Struct1) Add() {

14 a.A = a.A + 1

15 a.B = a.B + 1

16 }

17

18 func (a *Struct1) Del() {

19 a.A = a.A - 1

20 a.B = a.B - 1

21 }

22

23 func main() {

24 a := new(Struct1)

25 var i I

26 i = a

27

28 i.Add()

29 i.Del()

30 }

db9b0cebde85

image

这些内存地址都可以使用gdb调试时得到

(gdb) p i

$11 = {tab = 0x10a70e0 , data = 0xc42001a0c0}

(gdb) p a

$12 = (struct main.Struct1 *) 0xc42001a0c0

(gdb) p i.tab

$13 = (runtime.itab *) 0x10a70e0

(gdb) p i.tab.inter

$14 = (runtime.interfacetype *) 0x105dc60

(gdb) p i.tab._type

$15 = (runtime._type *) 0x105d200

通过对内存地址的打印,可以很清晰的看出在对有方法的interface变量进行赋值时的内存分布。Struct1类型和interface I类型都存在内存记录着各自的_type结构体信息,在将Struct1类型的变量赋值给interface I类型时,会有一个itab类型的结构体将Struct1类型和interface I类型关联起来。

上面的例子都是将一个指针赋值给interface变量,如果是将一个值赋值给interface变量。会先对分配一块空间保存该值的副本,然后将该interface变量的data字段指向这个新分配的空间。将一个值赋值给interface变量时,操作的都是该值的一个副本。

2.3 方法的调用

上面对有方法的interface进行赋值后,是如何实现通过接口变量实现了函数调用呢?参考下面的汇编代码

1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/iface.go

2 iface.go:23 0x104f3e0 65488b0c25a0080000 MOVQ GS:0x8a0, CX

3 iface.go:23 0x104f3e9 483b6110 CMPQ 0x10(CX), SP

4 iface.go:23 0x104f3ed 0f8687000000 JBE 0x104f47a

5 iface.go:23 0x104f3f3 4883ec38 SUBQ $0x38, SP

6 iface.go:23 0x104f3f7 48896c2430 MOVQ BP, 0x30(SP)

7 iface.go:23 0x104f3fc 488d6c2430 LEAQ 0x30(SP), BP

8 iface.go:23 0x104f401 488d0578ff0000 LEAQ 0xff78(IP), AX

9 iface.go:24 0x104f408 48890424 MOVQ AX, 0(SP)

10 iface.go:24 0x104f40c e86fcefbff CALL runtime.newobject(SB)

11 iface.go:24 0x104f411 488b442408 MOVQ 0x8(SP), AX

12 iface.go:24 0x104f416 4889442410 MOVQ AX, 0x10(SP)

13 iface.go:25 0x104f41b 48c744242000000000 MOVQ $0x0, 0x20(SP)

14 iface.go:25 0x104f424 48c744242800000000 MOVQ $0x0, 0x28(SP)

15 iface.go:26 0x104f42d 488b442410 MOVQ 0x10(SP), AX

16 iface.go:26 0x104f432 4889442418 MOVQ AX, 0x18(SP)

17 iface.go:26 0x104f437 488d0da27c0500 LEAQ 0x57ca2(IP), CX

18 iface.go:26 0x104f43e 48894c2420 MOVQ CX, 0x20(SP)

19 iface.go:26 0x104f443 4889442428 MOVQ AX, 0x28(SP)

20 iface.go:28 0x104f448 488b442420 MOVQ 0x20(SP), AX

21 iface.go:28 0x104f44d 488b4020 MOVQ 0x20(AX), AX

22 iface.go:28 0x104f451 488b4c2428 MOVQ 0x28(SP), CX

23 iface.go:28 0x104f456 48890c24 MOVQ CX, 0(SP)

24 iface.go:28 0x104f45a ffd0 CALL AX

25 iface.go:29 0x104f45c 488b442420 MOVQ 0x20(SP), AX

26 iface.go:29 0x104f461 488b4028 MOVQ 0x28(AX), AX

27 iface.go:29 0x104f465 488b4c2428 MOVQ 0x28(SP), CX

28 iface.go:29 0x104f46a 48890c24 MOVQ CX, 0(SP)

29 iface.go:29 0x104f46e ffd0 CALL AX

30 iface.go:30 0x104f470 488b6c2430 MOVQ 0x30(SP), BP

31 iface.go:30 0x104f475 4883c438 ADDQ $0x38, SP

32 iface.go:30 0x104f479 c3 RET

33 iface.go:23 0x104f47a e8f182ffff CALL runtime.morestack_noctxt(SB)

34 iface.go:23 0x104f47f e95cffffff JMP main.main(SB)

汇编代码的第17行和18行,将itab的地址加载到SP+0x20地址处,第20,21行,24行将SP+0x20的值加载到AX寄存器,然后将AX+0x20地址的值加载到AX寄存器,CALL AX就实现了add方法的调用,其中第22行和23行的作用是将interface里面data字段的地址传递给了add方法。

db9b0cebde85

image

通过对itab结构体进行分析,可以看到偏移0x20处为fun字段,其中0x20处为add函数的入口地址,0x28处就是del函数的入口地址。

2.4 断言的实现

在Go语言中,经常需要对一个interface变量进行断言

1 package main

2

3 type Struct1 struct {

4 A int64

5 }

6

7 func main() {

8 a := new(Struct1)

9

10 var i interface{}

11 i = a

12

13 b, ok := i.(Struct1)

14 if ok {

15 _ = b

16 }

17 }

生成汇编代码进行分析

1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/assert.go

2 assert.go:7 0x104f360 4883ec48 SUBQ $0x48, SP

3 assert.go:7 0x104f364 48896c2440 MOVQ BP, 0x40(SP)

4 assert.go:7 0x104f369 488d6c2440 LEAQ 0x40(SP), BP

5 assert.go:8 0x104f36e 48c744241000000000 MOVQ $0x0, 0x10(SP)

6 assert.go:8 0x104f377 488d442410 LEAQ 0x10(SP), AX

7 assert.go:8 0x104f37c 4889442420 MOVQ AX, 0x20(SP)

8 assert.go:10 0x104f381 48c744243000000000 MOVQ $0x0, 0x30(SP)

9 assert.go:10 0x104f38a 48c744243800000000 MOVQ $0x0, 0x38(SP)

10 assert.go:11 0x104f393 488b442420 MOVQ 0x20(SP), AX

11 assert.go:11 0x104f398 4889442428 MOVQ AX, 0x28(SP)

12 assert.go:11 0x104f39d 488d0d1c680000 LEAQ 0x681c(IP), CX

13 assert.go:11 0x104f3a4 48894c2430 MOVQ CX, 0x30(SP)

14 assert.go:11 0x104f3a9 4889442438 MOVQ AX, 0x38(SP)

15 assert.go:13 0x104f3ae 488b442438 MOVQ 0x38(SP), AX

16 assert.go:13 0x104f3b3 488b4c2430 MOVQ 0x30(SP), CX

17 assert.go:13 0x104f3b8 488d1581ed0000 LEAQ 0xed81(IP), DX

18 assert.go:13 0x104f3bf 4839d1 CMPQ DX, CX

19 assert.go:13 0x104f3c2 7402 JE 0x104f3c6

20 assert.go:13 0x104f3c4 eb3f JMP 0x104f405

21 assert.go:13 0x104f3c6 488b00 MOVQ 0(AX), AX

22 assert.go:13 0x104f3c9 b901000000 MOVL $0x1, CX

23 assert.go:13 0x104f3ce eb00 JMP 0x104f3d0

汇编的第12行,17行,18行可以看出,将Struct1对应的_type结构体的地址赋值给interface以后。在进行断言的时候,原理就是将interface变量_type字段的与Struct1对应的_type结构地址进行对比。

在本例子中,第12行的IP寄存器对应的值是0x104f39d,0x681c(IP)对应的地址为0x1055BB9,第17行的IP寄存器对应的值是0x104f3b8,0xed81(IP)对应的地址为0x105E139,貌似并不相同。可能是对Go的汇编中对IP寄存器的理解存在偏差,找了几个小时资料都没找到原因。

3 Go的反射

反射是一种强大的语言特性,可以“动态”的调用方法,获取结构体运行时的一些特征,很多框架的实现都离不开反射。Go的反射就是通过interface类型来实现的。

3.1 反射获取变量的信息

Go的反射包主要存在两个重要的结构体。

1 type Value struct {

2 typ *rtype

3 ptr unsafe.Pointer

4 flag

5 }

6

7 func ValueOf(i interface{}) Value {

8 }

9

10 type Type interface {

11 Align() int

12 FieldAlign() int

13 Method(int) Method

14 Name() string

15 //一堆方法

16 //....

17 }

18

19 func TypeOf(i interface{}) Type {

20 eface := *(*emptyInterface)(unsafe.Pointer(&i))

21 return toType(eface.typ)

22 }

23

24 type emptyInterface struct {

25 typ *rtype

26 word unsafe.Pointer

27 }

任何一个变量可以通过调用ValueOf来获取到变量的Value结构体,通过TypeOf方法来获取变量的Type接口类型。通过TypeOf方法获取到的Type接口实际上就是该变量对应的_type。

通过前面的分析,当通过TypeOf方法获取到变量的_type结构体后,很容易获取到该变量的一些基本信息,比如_type结构体中的各种字段都可以直接获取到。

3.2 反射修改变量的值

1 package main

2

3 import (

4 "reflect"

5 )

6

7 func main() {

8 var x int64 = 10

9

10 reflect.ValueOf(x).SetInt(20)

11

12 reflect.ValueOf(&x).SetInt(20)

13

14 reflect.ValueOf(&x).Elem().SetInt(20)

15 }

上面的例子中,第10行,12行都会报panic,只有第14行能修改变量的值。在使用ValueOf获取到Value结构体以后,flag字段记录着值能否进行修改,这样应该是为了避免误操作,保证api调用者明确了解到是否需要修改值。

3.3 反射修改结构体变量字段的值

如果需要通过反射修改某结构体里面各个字段的值。

1 package main

2

3 import (

4 "reflect"

5 "fmt"

6 )

7

8 type Struct1 struct {

9 A int64

10 B int64

11 C int64

12 }

13

14 func main() {

15 P := new(Struct1)

16

17 V := reflect.ValueOf(P).Elem()

18 V.FieldByName("A").SetInt(100)

19 V.FieldByName("B").SetInt(200)

20 V.FieldByName("C").SetInt(300)

21

22 fmt.Printf("%v", P)

23 }

上面的代码中,需要根据结构体字段的名称对各个字段的值进行修改,内部是如何实现的呢?

db9b0cebde85

image

每一个自定义的struct类型都存在这一个对应的structType结构体,该结构体记录了每个字段structField。通过对比structField里面的name字段,就可以获取到某个字段的type和偏移量。从而对具体的值进行修改。

3.4 反射动态调用方法

动态的调用方法是怎么实现的?

1 package main

2

3 import (

4 "reflect"

5 )

6

7 type Struct1 struct {

8 A int64

9 B int64

10 C int64

11 }

12

13 func (p *Struct1) Set() {

14 p.A = 200

15 }

16

17 func main() {

18 P := new(Struct1)

19 P.A = 100

20 P.B = 200

21 P.C = 300

22

23 V := reflect.ValueOf(P)

24

25 params := make([]reflect.Value, 0)

26 V.MethodByName("Set").Call(params)

27 }

结构体的方法在内存中存在如下的分布

db9b0cebde85

image

在编译过程中,结构体对应方法的相关信息都已经存在于内存中,分配了一块uncommonType的结构体跟在fields字段后面。根据内存的分布,如果需要根据一个结构体的名称获取到方法并且执行,只需要根据uncommonType结构中的moff字段去获取方法相关信息的地址块,然后逐个对比名称是否为想要获取的方法进行调用。

4 总结

本文从实现原理上分析了Go语言中interface类型和反射包的使用,相信各位读者以后再使用Go的interface类型和反射包时能做到胸有成竹,也能够对分析Go语言的其它特性提供思路。

作者:喻家山车神

链接:https://www.jianshu.com/p/70003e0f49d1

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值