• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

Go语言interface实现原理详解

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

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地址。
 具体过程如下图:

 
go函数调用.jpg

 

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文件中查看。

 
iface和eface的内存分布.jpg

 

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

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


 
没有方法的interface赋值.jpg
  • 有方法的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  }
 
有方法的interface赋值.jpg

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

(gdb) p i
$11 = {tab = 0x10a70e0 <Struct1,main.I>, data = 0xc42001a0c0}
(gdb) p a
$12 = (struct main.Struct1 *) 0xc42001a0c0
(gdb) p i.tab
$13 = (runtime.itab *) 0x10a70e0 <Struct1,main.I>
(gdb) p i.tab.inter
$14 = (runtime.interfacetype *) 0x105dc60 <type.*+59232>
(gdb) p i.tab._type
$15 = (runtime._type *) 0x105d200 <type.*+56576>

 通过对内存地址的打印,可以很清晰的看出在对有方法的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

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
[golang] go 常用命令发布时间:2022-07-10
下一篇:
go语言redis操作——redigo发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap