脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - 一文弄懂Go内存中的结构体

一文弄懂Go内存中的结构体

2021-12-21 00:45Golang In Memoryfooree Golang

在数据存储上来讲,结构体和数组没有太大的区别. 只不过结构体的各个字段(元素)类型可以相同,也可以不同,所以只能通过字段的相对偏移量进行访问.

一文弄懂Go内存中的结构体

结构体

所谓结构体,实际上就是由各种类型的数据组合而成的一种复合数据类型.

在数据存储上来讲,结构体和数组没有太大的区别. 只不过结构体的各个字段(元素)类型可以相同,也可以不同,所以只能通过字段的相对偏移量进行访问. 而数组的各个元素类型相同,可以通过索引快速访问,实际其本质上也是通过相对偏移量计算地址进行访问.

因为结构体的各个字段类型不同,有大有小,而结构体在存储时通常需要进行内存对齐,所以结构体在存储时可能会出现"空洞",也就是无法使用到的内存空间.

在之前的Go系列文章中,我们接触最多的结构体是reflect包中的rtype,可以说已经非常熟悉.

  1. type rtype struct {
  2. size uintptr
  3. ptrdata uintptr // number of bytes in the type that can contain pointers
  4. hash uint32 // hash of type; avoids computation in hash tables
  5. tflag tflag // extra type information flags
  6. align uint8 // alignment of variable with this type
  7. fieldAlign uint8 // alignment of struct field with this type
  8. kind uint8 // enumeration for C
  9. equal func(unsafe.Pointer, unsafe.Pointer) bool
  10. gcdata *byte // garbage collection data
  11. str nameOff // string form
  12. ptrToThis typeOff // type for pointer to this type, may be zero
  13. }

在64位程序和系统中占48个字节,其结构分布如下:

一文弄懂Go内存中的结构体

在Go语言中,使用reflect.rtype结构体描述任何Go类型的基本信息.

在Go语言中,使用reflect.structType结构体描述结构体类别(reflect.Struct)数据的类型信息,定义如下:

  1. // structType represents a struct type.
  2. type structType struct {
  3. rtype
  4. pkgPath name
  5. fields []structField // sorted by offset
  6. }
  7. // Struct field
  8. type structField struct {
  9. name name // name is always non-empty
  10. typ *rtype // type of field
  11. offsetEmbed uintptr // byte offset of field<<1 | isEmbedded
  12. }

在64位程序和系统中占80个字节,其结构分布如下:

一文弄懂Go内存中的结构体

在之前的几篇文章中,已经详细介绍了类型方法相关内容,如果还未阅读,建议不要错过:

  • 再谈整数类型
  • 深入理解函数
  • 内存中的接口类型

在Go语言中,结构体类型不但可以包含字段,还可以定义方法,实际上完整的类型信息结构分布如下:

一文弄懂Go内存中的结构体

当然,结构体是可以不包含字段的,也可以没有方法的.

环境

  1. OS : Ubuntu 20.04.2 LTS; x86_64
  2. Go : go version go1.16.2 linux/amd64

声明

操作系统、处理器架构、Go版本不同,均有可能造成相同的源码编译后运行时的寄存器值、内存地址、数据结构等存在差异。

本文仅包含 64 位系统架构下的 64 位可执行程序的研究分析。

本文仅保证学习过程中的分析数据在当前环境下的准确有效性。

代码清单

在Go语言中,结构体随处可见,所以本文示例代码中不再自定义结构体,而是使用Go语言中常用的结构体用于演示.

在 命令行参数详解 一文中,曾详细介绍过flag.FlagSet结构体.

本文,我们将详细介绍flag.FlagSet和reflect.Value两个结构体的类型信息.

  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "reflect"
  6. )
  7. func main() {
  8. f := flag.FlagSet{}
  9. Print(reflect.TypeOf(f))
  10. Print(reflect.TypeOf(&f))
  11. _ = f.Set("hello", "world")
  12. f.PrintDefaults()
  13. fmt.Println(f.Args())
  14. v := reflect.ValueOf(f)
  15. Print(reflect.TypeOf(v))
  16. Print(reflect.TypeOf(&v))
  17. Print(reflect.TypeOf(struct{}{}))
  18. }
  19. //go:noinline
  20. func Print(t reflect.Type) {
  21. fmt.Printf("Type = %s\t, address = %p\n", t, t)
  22. }

运行

一文弄懂Go内存中的结构体

从运行结果可以看到:

  • 结构体flag.FlagSet的类型信息保存在0x4c2ac0地址处.
  • 结构体指针*flag.FlagSet的类型信息保存在0x4c68e0地址处.
  • 结构体reflect.Value的类型信息保存在0x4ca160地址处.
  • 结构体指针*reflect.Value的类型信息保存在0x4c9c60地址处.
  • 匿名结构体struct{}{}的类型信息保存在0x4b4140地址处.

内存分析

在main函数入口处设置断点进行调试.我们先从简单的结构体开始分析.

匿名结构体struct{}

该结构体既没有字段,也没有方法,其类型信息数据如下:

一文弄懂Go内存中的结构体

  • rtype.size = 0x0 (0)
  • rtype.ptrdata = 0x0 (0)
  • rtype.hash = 0x27f6ac1b
  • rtype.tflag = tflagExtraStar | tflagRegularMemory
  • rtype.align = 1
  • rtype.fieldAlign = 1
  • rtype.kind = 0x19 (25) -> reflect.Struct
  • rtype.equal = 0x4d3100 -> runtime.memequal0
  • rtype.gcdata = 0x4ea04f
  • rtype.str = 0x0000241f -> "struct {}"
  • rtype.ptrToThis = 0x0 (0x0)
  • structType.pkgPath = 0 -> ""
  • structType.fields = []

这是一个特殊的结构体,没有字段,没有方法,不占用内存空间,明明定义在main包中,但是包路径信息为空,存储结构分布如下:

一文弄懂Go内存中的结构体

好神奇的是,struct{}类型的对象居然是可以比较的,其比较函数是runtime.memequal0,定义如下:

  1. func memequal0(p, q unsafe.Pointer) bool {
  2. return true
  3. }

也就是说,所有的struct{}类型的对象,无论它们在内存的什么位置,无论它们是在什么时间创建的,永远都是相等的.

细细品,还是蛮有道理的.

结构体类型flag.FlagSet

结构体flag.FlagSet包含8个字段,其类型信息占用288个字节.

一文弄懂Go内存中的结构体

  • rtype.size = 0x60 (96)
  • rtype.ptrdata = 0x60 (96)
  • rtype.hash = 0x644236d1
  • rtype.tflag = tflagUncommon | tflagExtraStar | tflagNamed
  • rtype.align = 8
  • rtype.fieldAlign = 8
  • rtype.kind = 0x19 (25) -> reflect.Struct
  • rtype.equal = nil
  • rtype.gcdata = 0x4e852c
  • rtype.str = 0x32b0 -> "flag.FlagSet"
  • rtype.ptrToThis = 0x208e0 (0x4c68e0)
  • structType.pkgPath = 0x4a6368 -> "flag"
  • structType.fields.Data = 0x4c2b20
  • structType.fields.Len = 8 -> 字段数量
  • structType.fields.Cap = 8
  • uncommonType.pkgpath = 0x368 -> "flag"
  • uncommonType.mcount = 0 -> 方法数量
  • uncommonType.xcount = 0
  • uncommonType.moff = 208
  • structType.fields =
  1. [
  2. {
  3. name = 0x4a69a0 -> Usage
  4. typ = 0x4b0140 -> func()
  5. offsetEmbed = 0x0 (0)
  6. },
  7. {
  8. name = 0x4a69a0 -> name
  9. typ = 0x4b1220 -> string
  10. offsetEmbed = 0x8 (8)
  11. },
  12. {
  13. name = 0x4a704a -> parsed
  14. typ = 0x4b0460 -> bool
  15. offsetEmbed = 0x18 (24)
  16. },
  17. {
  18. name = 0x4a6e64 -> actual
  19. typ = 0x4b4c20 -> map[string]*flag.Flag
  20. offsetEmbed = 0x20 (32)
  21. },
  22. {
  23. name = 0x4a6f0f -> formal
  24. typ = 0x4b4c20 -> map[string]*flag.Flag
  25. offsetEmbed = 0x28 (40)
  26. },
  27. {
  28. name = 0x4a646d -> args
  29. typ = 0x4afe00 -> []string
  30. offsetEmbed = 0x30 (48)
  31. },
  32. {
  33. name = 0x4a9450 -> errorHandling
  34. typ = 0x4b05a0 -> flag.ErrorHandling
  35. offsetEmbed = 0x48 (72)
  36. },
  37. {
  38. name = 0x4a702f -> output
  39. typ = 0x4b65c0 -> io.Writer
  40. offsetEmbed = 0x50 (80)
  41. }
  42. ]

从以上数据可以看到,结构体flag.FlagSet类型的数据对象,占用96字节的存储空间,并且所有字段全部被视为指针数据.

flag.FlagSet类型的对象不可比较,因为其rtype.equal字段值nil. 除了struct{}这个特殊的结构体类型,估计是不容易找到可比较的结构体类型了.

从以上字段数据可以看到,FlagSet.parsed字段的偏移量是24,FlagSet.actual字段的偏移量是32;也就是说,bool类型的FlagSet.parsed字段实际占用8字节的存储空间.

bool类型的实际值只能是0或1,只需要占用一个字节即可,实际的机器指令也会读取一个字节. 也就是,flag.FlagSet类型的对象在存储时,因为8字节对齐,此处需要浪费7个字节的空间.

从以上字段数据可以看到,string类型的字段占16个字节,[]string类型的字段占24个字节,接口类型的字段占16个字节,与之前文章中分析得到的结果一直.

另外,可以看到map类型的字段,实际占用8个字节的空间,在之后的文章中将会详细介绍map类型.

仔细的读者可能已经注意到,flag.FlagSet类型没有任何方法,因为其uncommonType.mcount = 0.

在flag/flag.go源文件中,不是定义了很多方法吗?

以上代码清单中,flag.FlagSet类型的对象f为什么可以调用以下方法呢?

  1. _ = f.Set("hello", "world")
  2. f.PrintDefaults()
  3. fmt.Println(f.Args())

实际上,flag/flag.go源文件中定义的方法的receiver都是*flag.FlagSet指针类型,没有flag.FlagSet类型.

  1. // Args returns the non-flag arguments.
  2. func (f *FlagSet) Args() []string { return f.args }

flag.FlagSet类型的对象f能够调用*flag.FlagSet指针类型的方法,只不过是编译器为方便开发者实现的语法糖而已.

在本例中,编译器会把flag.FlagSet类型的对象f的地址作为参数传递给*flag.FlagSet指针类型的方法.反之,编译器也是支持的.

指针类型*flag.FlagSet

为了方便查看类型信息,笔者开发了一个gdb的插件脚本.

查看*flag.FlagSet类型的信息如下,共包含38个方法,其中34个是公共方法.此处不再一一介绍.

  1. (gdb) info type 0x4c68e0
  2. interfaceType {
  3. rtype = {
  4. size = 0x8 (8)
  5. ptrdata = 0x8 (8)
  6. hash = 0xe05aa02c
  7. tflag = tflagUncommon | tflagRegularMemory
  8. align = 8
  9. fieldAlign = 8
  10. kind = ptr
  11. equal = 0x403a00
  12. gcdata = 0x4d2e28
  13. str = *flag.FlagSet
  14. ptrToThis = 0x0 (0x0)
  15. }
  16. elem = 0x4c2ac0 -> flag.FlagSet
  17. }
  18. uncommonType {
  19. pkgpath = flag
  20. mcount = 38
  21. xcount = 34
  22. moff = 16
  23. }
  24. methods [
  25. {
  26. name = Arg
  27. mtyp = nil
  28. ifn = nil
  29. tfn = nil
  30. },
  31. {
  32. name = Args
  33. mtyp = nil
  34. ifn = nil
  35. tfn = nil
  36. },
  37. {
  38. name = Bool
  39. mtyp = nil
  40. ifn = nil
  41. tfn = nil
  42. },
  43. {
  44. name = BoolVar
  45. mtyp = nil
  46. ifn = nil
  47. tfn = nil
  48. },
  49. {
  50. name = Duration
  51. mtyp = nil
  52. ifn = nil
  53. tfn = nil
  54. },
  55. {
  56. name = DurationVar
  57. mtyp = nil
  58. ifn = nil
  59. tfn = nil
  60. },
  61. {
  62. name = ErrorHandling
  63. mtyp = nil
  64. ifn = nil
  65. tfn = nil
  66. },
  67. {
  68. name = Float64
  69. mtyp = nil
  70. ifn = nil
  71. tfn = nil
  72. },
  73. {
  74. name = Float64Var
  75. mtyp = nil
  76. ifn = nil
  77. tfn = nil
  78. },
  79. {
  80. name = Func
  81. mtyp = nil
  82. ifn = nil
  83. tfn = nil
  84. },
  85. {
  86. name = Init
  87. mtyp = nil
  88. ifn = nil
  89. tfn = nil
  90. },
  91. {
  92. name = Int
  93. mtyp = nil
  94. ifn = nil
  95. tfn = nil
  96. },
  97. {
  98. name = Int64
  99. mtyp = nil
  100. ifn = nil
  101. tfn = nil
  102. },
  103. {
  104. name = Int64Var
  105. mtyp = nil
  106. ifn = nil
  107. tfn = nil
  108. },
  109. {
  110. name = IntVar
  111. mtyp = nil
  112. ifn = nil
  113. tfn = nil
  114. },
  115. {
  116. name = Lookup
  117. mtyp = nil
  118. ifn = nil
  119. tfn = nil
  120. },
  121. {
  122. name = NArg
  123. mtyp = 0x4b0960 -> func() int
  124. ifn = nil
  125. tfn = nil
  126. },
  127. {
  128. name = NFlag
  129. mtyp = 0x4b0960 -> func() int
  130. ifn = nil
  131. tfn = nil
  132. },
  133. {
  134. name = Name
  135. mtyp = 0x4b0b20 -> func() string
  136. ifn = 0x4a36e0 Name>
  137. tfn = 0x4a36e0 Name>
  138. },
  139. {
  140. name = Output
  141. mtyp = nil
  142. ifn = nil
  143. tfn = nil
  144. },
  145. {
  146. name = Parse
  147. mtyp = nil
  148. ifn = nil
  149. tfn = nil
  150. },
  151. {
  152. name = Parsed
  153. mtyp = 0x4b0920 -> func() bool
  154. ifn = nil
  155. tfn = nil
  156. },
  157. {
  158. name = PrintDefaults
  159. mtyp = 0x4b0140 -> func()
  160. ifn = 0x4a3ec0
  161. tfn = 0x4a3ec0
  162. },
  163. {
  164. name = Set
  165. mtyp = nil
  166. ifn = 0x4a37a0 Set>
  167. tfn = 0x4a37a0 Set>
  168. },
  169. {
  170. name = SetOutput
  171. mtyp = nil
  172. ifn = nil
  173. tfn = nil
  174. },
  175. {
  176. name = String
  177. mtyp = nil
  178. ifn = nil
  179. tfn = nil
  180. },
  181. {
  182. name = StringVar
  183. mtyp = nil
  184. ifn = nil
  185. tfn = nil
  186. },
  187. {
  188. name = Uint
  189. mtyp = nil
  190. ifn = nil
  191. tfn = nil
  192. },
  193. {
  194. name = Uint64
  195. mtyp = nil
  196. ifn = nil
  197. tfn = nil
  198. },
  199. {
  200. name = Uint64Var
  201. mtyp = nil
  202. ifn = nil
  203. tfn = nil
  204. },
  205. {
  206. name = UintVar
  207. mtyp = nil
  208. ifn = nil
  209. tfn = nil
  210. },
  211. {
  212. name = Var
  213. mtyp = nil
  214. ifn = nil
  215. tfn = nil
  216. },
  217. {
  218. name = Visit
  219. mtyp = nil
  220. ifn = nil
  221. tfn = nil
  222. },
  223. {
  224. name = VisitAll
  225. mtyp = nil
  226. ifn = 0x4a3700
  227. tfn = 0x4a3700
  228. },
  229. {
  230. name = defaultUsage
  231. mtyp = 0x4b0140 -> func()
  232. ifn = 0x4a3f20
  233. tfn = 0x4a3f20
  234. },
  235. {
  236. name = failf
  237. mtyp = nil
  238. ifn = nil
  239. tfn = nil
  240. },
  241. {
  242. name = parseOne
  243. mtyp = nil
  244. ifn = nil
  245. tfn = nil
  246. },
  247. {
  248. name = usage
  249. mtyp = 0x4b0140 -> func()
  250. ifn = nil
  251. tfn = nil
  252. }
  253. ]

结构体类型reflect.Value

实际上,编译器比想象的做的更多.

有时候,编译器会把源代码中的一个方法,编译出两个可执行的方法.在 内存中的接口类型 一文中,曾进行了详细分析.

直接运行gdb脚本查看reflect.Value类型信息,有3个字段,75个方法,此处为方便展示,省略了大部分方法信息.

  1. (gdb) info type 0x4ca160
  2. structType {
  3. rtype = {
  4. size = 0x18 (24)
  5. ptrdata = 0x10 (16)
  6. hash = 0x500c1abc
  7. tflag = tflagUncommon | tflagExtraStar | tflagNamed | tflagRegularMemory
  8. align = 8
  9. fieldAlign = 8
  10. kind = struct
  11. equal = 0x402720
  12. gcdata = 0x4d2e48
  13. str = reflect.Value
  14. ptrToThis = 0x23c60 (0x4c9c60)
  15. }
  16. pkgPath = reflect
  17. fields = [
  18. {
  19. name = 0x4875094 -> typ
  20. typ = 0x4c6e60 -> *reflect.rtype
  21. offsetEmbed = 0x0 (0)
  22. },
  23. {
  24. name = 0x4874896 -> ptr
  25. typ = 0x4b13e0 -> unsafe.Pointer
  26. offsetEmbed = 0x8 (8)
  27. },
  28. {
  29. name = 0x4875112 -> flag
  30. typ = 0x4be7c0 -> reflect.flag
  31. offsetEmbed = 0x10 (16) embed
  32. }
  33. ]
  34. }
  35. uncommonType {
  36. pkgpath = reflect
  37. mcount = 75
  38. xcount = 61
  39. moff = 88
  40. }
  41. methods [
  42. {
  43. name = Addr
  44. mtyp = nil
  45. ifn = nil
  46. tfn = nil
  47. },
  48. {
  49. name = Bool
  50. mtyp = 0x4b0920 -> func() bool
  51. ifn = nil
  52. tfn = 0x4881c0
  53. },
  54. ......
  55. {
  56. name = Kind
  57. mtyp = 0x4b0aa0 -> func() reflect.Kind
  58. ifn = 0x48d500
  59. tfn = 0x489400
  60. },
  61. {
  62. name = Len
  63. mtyp = 0x4b0960 -> func() int
  64. ifn = 0x48d560
  65. tfn = 0x489420
  66. },
  67. ......
  68. ]

再看*reflect.Value指针类型的信息,没有任何字段(毕竟是指针),也有75个方法.

  1. (gdb) info type 0x4c9c60
  2. interfaceType {
  3. rtype = {
  4. size = 0x8 (8)
  5. ptrdata = 0x8 (8)
  6. hash = 0xf764ad0
  7. tflag = tflagUncommon | tflagRegularMemory
  8. align = 8
  9. fieldAlign = 8
  10. kind = ptr
  11. equal = 0x403a00
  12. gcdata = 0x4d2e28
  13. str = *reflect.Value
  14. ptrToThis = 0x0 (0x0)
  15. }
  16. elem = 0x4ca160 -> reflect.Value
  17. }
  18. uncommonType {
  19. pkgpath = reflect
  20. mcount = 75
  21. xcount = 61
  22. moff = 16
  23. }
  24. methods [
  25. {
  26. name = Addr
  27. mtyp = nil
  28. ifn = nil
  29. tfn = nil
  30. },
  31. {
  32. name = Bool
  33. mtyp = 0x4b0920 -> func() bool
  34. ifn = nil
  35. tfn = nil
  36. },
  37. ......
  38. {
  39. name = Kind
  40. mtyp = 0x4b0aa0 -> func() reflect.Kind
  41. ifn = 0x48d500
  42. tfn = 0x48d500
  43. },
  44. {
  45. name = Len
  46. mtyp = 0x4b0960 -> func() int
  47. ifn = 0x48d560
  48. tfn = 0x48d560
  49. },
  50. ......
  51. ]

我们可以清楚地看到,在源码中Len()方法,编译之后,生成了两个可执行方法,分别是:

  • reflect.Value.Len
  • reflect.(*Value).Len
  1. func (v Value) Len() int {
  2. k := v.kind()
  3. switch k {
  4. case Array:
  5. tt := (*arrayType)(unsafe.Pointer(v.typ))
  6. return int(tt.len)
  7. case Chan:
  8. return chanlen(v.pointer())
  9. case Map:
  10. return maplen(v.pointer())
  11. case Slice:
  12. // Slice is bigger than a word; assume flagIndir.
  13. return (*unsafeheader.Slice)(v.ptr).Len
  14. case String:
  15. // String is bigger than a word; assume flagIndir.
  16. return (*unsafeheader.String)(v.ptr).Len
  17. }
  18. panic(&ValueError{"reflect.Value.Len", v.kind()})
  19. }

通过reflect.Value类型的对象调用时,实际可能执行的两个方法中的任何一个.

通过*reflect.Value类型的指针对象调用时,也可能执行的两个方法中的任何一个.

这完全是由编译器决定的.

但是通过接口调用时,执行的一定是reflect.(*Value).Len这个方法的指令集合.

自定义结构体千变万化,但是结构体类型信息相对还是单一,容易理解.

原文链接:https://mp.weixin.qq.com/s/i9rtzEjAr8R14QxUPn6Hig

延伸 · 阅读

精彩推荐
  • Golanggolang 通过ssh代理连接mysql的操作

    golang 通过ssh代理连接mysql的操作

    这篇文章主要介绍了golang 通过ssh代理连接mysql的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    a165861639710342021-03-08
  • Golanggo日志系统logrus显示文件和行号的操作

    go日志系统logrus显示文件和行号的操作

    这篇文章主要介绍了go日志系统logrus显示文件和行号的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    SmallQinYan12302021-02-02
  • Golanggolang json.Marshal 特殊html字符被转义的解决方法

    golang json.Marshal 特殊html字符被转义的解决方法

    今天小编就为大家分享一篇golang json.Marshal 特殊html字符被转义的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    李浩的life12792020-05-27
  • Golanggolang如何使用struct的tag属性的详细介绍

    golang如何使用struct的tag属性的详细介绍

    这篇文章主要介绍了golang如何使用struct的tag属性的详细介绍,从例子说起,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看...

    Go语言中文网11352020-05-21
  • GolangGolang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    天易独尊11682021-06-09
  • Golanggo语言制作端口扫描器

    go语言制作端口扫描器

    本文给大家分享的是使用go语言编写的TCP端口扫描器,可以选择IP范围,扫描的端口,以及多线程,有需要的小伙伴可以参考下。 ...

    脚本之家3642020-04-25
  • Golanggolang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    helight2992020-05-14
  • GolangGolang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由...

    4272021-11-24