Golang 数据结构到底是怎么回事?gdb调一调?

“ 不仅限于语法,使用gdb,dlv工具更深层的剖析golang的数据结构”

Golang数据结构

变量:有意义的一个数据块。
变量名:一个有意义的数据块的名字。

为什么特意会有这个章节?

golang本质是全局按照值传递的,也就是copy值,那么你必须知道你的内存对象是包含哪些内容,才能清楚掌握你在copy值的时候复制了哪些东西,这个很重要,第一部分的正文内容从这里开始。具体如下类型:

  • num
  • bool
  • string
  • array
  • slice
  • map
  • channel
  • interface

这些结构实际在地址空间栈是什么形式的实现?这里直接看地址空间的内容,以下都是以这个例子进行分析:

package main

func main () {
    var n int64 = 11
    var b bool = true

    var s string = "test-string-1"

    var a [3]bool = [3]bool{true, false, true}
    var sl []int = []int{1,2,3,4}

    var m map[int]string
    var c chan int
    var in interface{}

    _, _, _, _, _, _, _, _ = n, b, s, a, sl, m, c , in
}

数值类型

n:n就是一个8字节的数据块。

Bool类型

b:就是一个1字节的数据块。

String类型

string类型在go里是一个复合类型,s变量是一个16字节的变量。其中str字段指向字符串存储的地方。

(gdb) pt s
type = struct string {
  uint8 *str;
  int len;
}

换句话说,s就是代表了一个16字节的数据块。所以我们每次定义一个string变量,除了字符序列,s的本身结构是分配16个字节在栈上。

赋值语句

0x000000000044ebed <+61>:    lea    0x1e81d(%rip),%rax        # 0x46d411
0x000000000044ebf4 <+68>:    mov    %rax,0x48(%rsp)
0x000000000044ebf9 <+73>:    movq   $0xd,0x50(%rsp)

地址空间存储

(gdb) p/x uintptr(&s)
$9 = 0xc000030750
(gdb) x/2gx 0xc000030750
0xc000030750:    0x000000000046d411    0x000000000000000d
(gdb) x/s 0x000000000046d411
0x46d411:    "test-string-1triggerRatio=value method xadd64 failedxchg64 failed}\n\tsched={pc: but progSize  nmidlelocked= out of range  t.npagesKey=  untyped args -thread limit\nGC assist waitGC worker initMB; alloca"...
在这里插入图片描述

数组类型

数组类型,就是在地址空间中连续的一段内存块,和c一样(旁白:和c一样,都是平坦的内存结构)。

(gdb) p &a
$13 = ([3]bool *) 0xc00003070d
(gdb) x/6bx 0xc00003070d
0xc00003070d:    0x01    0x00    0x01    0x0b    0x00    0x00

Slice类型

这是个复合类型,变量本身是一个管理结构(和string一样),这个管理结构管理着一段连续的内存。

(gdb) pt sl
type = struct []int {
  int *array;
  int len;
  int cap;
}

map 类型 和 channel 类型

其中,map类型和channel类型特别提一点,变量本身本质上是一个指针类型。也就是说上面我们定义了两个变量m,c,从内存分配的角度来讲,只在栈上分配了一个指针变量,并且这个指针还是nil值,所以我们经常看到 go 的一个特殊说明:slice,map,channel 这三种类型必须使用make来创建,就是这个道理。因为如果仅仅定义了类型变量,那仅仅是代表了分配了这个变量本身的内存空间,并且初始化是nil,一旦你直接用,那么就会导致非法地址引用的问题。slice 的24个字节的管理空间,map和channel的一个指针8个字节的空间。那么如果是调用了make,其实就会把下面的结构分配并初始化出来。

(gdb) pt m
type = struct hash<int, string> {
  int count;
  uint8 flags;
  uint8 B;
  uint16 noverflow;
  uint32 hash0;
  struct bucket<int, string> *buckets;
  struct bucket<int, string> *oldbuckets;
  uintptr nevacuate;  
  struct runtime.mapextra *extra;
} *

(gdb) pt c
type = struct hchan<int> {
  uint qcount;
  uint dataqsiz;
  void *buf;
  uint16 elemsize;
  uint32 closed;
  runtime._type *elemtype;
  uint sendx;
  uint recvx;
  struct waitq<int> recvq;
  struct waitq<int> sendq;
  runtime.mutex lock;
} *

那么make又做了什么,make 是 golang 的关键字,编译器会自动转变成函数调用,go 语言的关键字基本都是转换成内部的函数调用,梳理golang的关键字对应的转换表:


在这里插入图片描述

(旁白:编译器牛逼,golang 比c 高级就高级在编译器帮你做了非常多的事情)

Interface

特别说明下interface,因为这个也是 golang 的一个特色点,你实现了interface定义的方法集,那么可以当作这个接口用,换句话说,你实现了这些行为,就是这个接口对象,怎么实现的?这个和c++的多态是很像的,和python的行为多态更像。c++是通过虚表结构来实现的多态。go实现的接口是通过interface结构来实现的。

回忆下 c++ 的多态,c++ 是静态就定义好了继承或组合关系,虚表的个数是确定的:

  1. 对象头部存在一个虚表指针
  2. 虚表的内容是编译器在编译期间就定好了的
  3. 每个类都是有自己的虚表的,不同类创建出来的对象虚表指针是指向自己类的虚表


    在这里插入图片描述

    有继承覆盖的情况如下:


    在这里插入图片描述

    所以 c++ 的多态就一目了然,虽然我们可能不知道当前对象是那个类,当时我们通过头部的虚表指针找到对应的虚表,按照一样的偏移offset去获取到函数指针,这个对象方法自然就会是我们对象正确的方法。
go interface

type iface struct {
  tab  *itab
  data unsafe.Pointer
}

type eface struct {
  _type *_type
  data  unsafe.Pointer
}

当我们把一个对象赋值给接口,调用方法的时候,接口怎么能获取到对象正确的方法? iface 接口里 data 存放私有数据,一般是具体对象地址。关键就在 itab 结构,本质上是一个pair(interface,concrete)

itab结构

这个结构会在两种情况下生成:

  • 编译期间( static in compile ):这个是大部分情况,只要是编译器能分析出来的,就会尽量在编译期间生成itab结构,然后存放在.rodata只读数据区域。比如类型赋值到接口的场景。(旁白:绝大部分是这种情况,只要编译器能帮你干的就顺手帮你干了,这种情况的运行时开销几乎没有,就是一个间接调用的开销)
  • 运行期间(runtime):有些场景是在编译期间无法确认itab的,这个只能等到runtime期间,动态的去查找、获取、生成。比如接口和接口直接相互转换的场景,这个就只能在运行期间才能确定

这种情况下,带来的开销是很大的,所以内部这种情况下是有对itab做一个全局缓存的,优化( interface 类型,具体类型)查找itab的性能

type itab struct {
   inter *interfacetype
   _type *_type
   hash  uint32 // copy of _type.hash. Used for type switches.
   _     [4]byte
  fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

字段含义:

  1. inter是描述接口类型的变量。
  2. _type是描述具体对象的类型对象。
  3. fun是一个可变数组,依次存放着这个pair(interface,concrete)实际的方法地址。

内存布局:


在这里插入图片描述

这么个结构如果是编译器生成,是存在.rodata里。

编译器静态生成 itab

举个例子:

package main

type Node interface {
    Add(a, b int32) int32
    Sub(a, b int64) int64
}

type SObj struct{ id int32 }
func (adder SObj) Add(a, b int32) int32 { return a + b }
func (adder SObj) Sub(a, b int64) int64 { return a - b }

func main() {
    m := Node(SObj{id: 6754})
    m.Add(10, 32)
}

这里通过 SObj 变量类型赋值到接口 Node. 首先呢,这个在编译期就会检查是否能够这样赋值,允许赋值的满足的要求就是:SObj实现了Node的声明的两个方法。编译通过之后呢,会生成一个二元组对pair(Node,SObj)。调用过程呢,就是先获取itab,在获取fun,根据偏移获取方法地址。看下汇编代码:

(gdb) disassemble
Dump of assembler code for function main.main:
  => 0x000000000044ec70 <+0>:    mov    %fs:0xfffffffffffffff8,%rcx
  0x000000000044ec79 <+9>:    cmp    0x10(%rcx),%rsp
  0x000000000044ec7d <+13>:    jbe    0x44ecf8 <main.main+136>  
  0x000000000044ec7f <+15>:    sub    $0x40,%rsp
  0x000000000044ec83 <+19>:    mov    %rbp,0x38(%rsp)
  0x000000000044ec88 <+24>:    lea    0x38(%rsp),%rbp
  0x000000000044ec8d <+29>:    movl   $0x0,0x24(%rsp)

  // 根据itab, val 构造出 interface结构 (这个是)
  0x000000000044ec95 <+37>:    movl   $0x1a62,0x24(%rsp)
  0x000000000044ec9d <+45>:    lea    0x2993c(%rip),%rax        # 0x4785e0 <go.itab.main.SObj,main.Node> // itab的地址;编译期确定
  0x000000000044eca4 <+52>:    mov    %rax,(%rsp)
  0x000000000044eca8 <+56>:    mov    0x24(%rsp),%eax           // value的值6754
  0x000000000044ecac <+60>:    mov    %eax,0x8(%rsp)
  0x000000000044ecb0 <+64>:    callq  0x407fd0 <runtime.convT2I32>
  0x000000000044ecb5 <+69>:    mov    0x18(%rsp),%rax
  0x000000000044ecba <+74>:    mov    0x10(%rsp),%rcx
  0x000000000044ecbf <+79>:    mov    %rcx,0x28(%rsp)           // interface m的地址
  0x000000000044ecc4 <+84>:    mov    %rax,0x30(%rsp)

  0x000000000044ecc9 <+89>:    mov    0x28(%rsp),%rax
  0x000000000044ecce <+94>:    test   %al,(%rax)
  0x000000000044ecd0 <+96>:    mov    0x18(%rax),%rax           // 获取到SObj.Add的地址
  0x000000000044ecd4 <+100>:    mov    0x30(%rsp),%rcx
  // 传参数 10,20
  0x000000000044ecd9 <+105>:    movabs $0x200000000a,%rdx
  0x000000000044ece3 <+115>:    mov    %rdx,0x8(%rsp)
  0x000000000044ece8 <+120>:    mov    %rcx,(%rsp)
  // 调用到 SObj.Add 方法
  0x000000000044ecec <+124>:    callq  *%rax
  0x000000000044ecee <+126>:    mov    0x38(%rsp),%rbp
  0x000000000044ecf3 <+131>:    add    $0x40,%rsp
  0x000000000044ecf7 <+135>:    retq
  0x000000000044ecf8 <+136>:    callq  0x446fb0 <runtime.morestack_noctxt>
  0x000000000044ecfd <+141>:    jmpq   0x44ec70 <main.main>
  End of assembler dump.

(gdb) p m
$3 = {tab = 0x4785e0 <SObj,main.Node>, data = 0xc00005e000}
(gdb) p &m
$4 = (main.Node *) 0xc000030778
(gdb) p $rsp + 0x28
$5 = (void *) 0xc000030778
(gdb) x/1gx 0x28 + $rsp
0xc000030778:    0x00000000004785e0
(gdb) x/1gx 0x00000000004785e0 + 0x18
0x4785f8 <go.itab.main.SObj,main.Node+24>:    0x000000000044ed70
(gdb) info symbol 0x000000000044ed70
main.(*SObj).Add in section .text of /root/go-proj/test_interface_1

查看二进制 .rodata 段

[root@bogon go-proj]# objdump -xt -j .rodata ./test_interface_1|grep SObj
00000000004785e0 g     O .rodata    0000000000000028 go.itab.main.SObj,main.Node

这里还要提一点,这里定义的两个方法reciver都是变量值,而不是指针,但其实golang默认的都是按照引用操作的,reciver为指针的版本一定会生成,取值则是按照反引用取值的。所以编译器其实生成了四个函数。

000000000044ec30 g     F .text    0000000000000015 main.SObj.Add
000000000044ed70 g     F .text    000000000000008c main.(*SObj).Add
000000000044ec50 g     F .text    0000000000000019 main.SObj.Sub
000000000044ee00 g     F .text    0000000000000097 main.(*SObj).Sub

业务逻辑是在 main.(SObj).Add和main.(SObj).Sub 里面,另外两个是包装函数。所以,当reciver为值的时候,你传指针或者值都可以。编译期可以帮你判定搞定。当reciver为引用的时候,你只能传引用,因为编译期没法搞定,因为这种情况就是假定我们可能要修改原对象,你如果传值,值copy之后,就不是原来的值了,这就违背了基本语义。

动态生成 itab

这个主要用在接口<->接口的场景。这种情况的itab表是动态生成的,编译期是没有静态生成的。这种两个都是接口的,你就必须是动态的去找,因为只有在runtime的时候才能知道里面赋值的到底是什么具体类型。对于itab会有一个全局hash缓存表,缓存优化性能。

举个例子:

package main

type Student struct { name string }

type Ione interface { getName() string }
type Itwo interface { getName() string }

func (s Student) getName() string { return s.name }
func main() {
var i Ione
var t Itwo

    s := Student{ name:"concrete obj" }

    i = s
    t = i // interface 2 interface

    _ = t
}

其中由于 i = s 是concrete类型到interface的赋值,这里编译器可以直接生成静态的itab结构。而由于 t = i 是接口到接口的赋值,这个编译器是无法去生成静态itab的(不要看到这里的简单例子,觉得自己肉眼可以在编译期间确定静态itab表。如果放到项目的大环境,比如纯操作接口的一个函数里面,你是无法分析出interface是否绑定到某个对象)。


在这里插入图片描述

(旁白:动态的有点是更灵活,缺点是现场性能损耗)

t = i 是调用 convI2I 转换函数生成 itab <Itwo, Student>。


在这里插入图片描述

关键路径:

convI2I -> getitab
            -> new itab & itab.init
            -> itabAdd

坚持思考,方向比努力更重要。关注我:奇伢云存储


![
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 307、setValue:forKey和setObject:forKey的区别是什么? 答:1, setObjec...
    AlanGe阅读 1,523评论 0 1
  • [TOC] Golang interface 全面介绍 interface 介绍 如果说goroutine和cha...
    AllenWu阅读 26,557评论 1 20
  • 重点掌握 3 类对象和方法 对象就是一个物体 类的独特存在就是一个实例,对实例进行操作叫做方法。方法可以应用于类或...
    Coder大雄阅读 1,254评论 0 2
  • 有人说,内裤就像知识,你必须得有,但不必逢人就证明你有! 所以,内裤是人的行装最最重要的部分,没有之一! 但有人质...
    迷你欧阅读 799评论 3 8
  • 前不久,我含泪把印度电影神秘巨一气呵成的看完。久久不能平静,这是一部在我看来难得的好电影。 好在哪些地方呢。 第一...
    牛德华12345阅读 290评论 0 0