Go 内存分布

简介: 该文章深入分析了Go语言中值的内存分布方式,特别是那些分布在多个内存块上的类型,如切片、映射、通道、函数、接口和字符串,并讨论了这些类型的内部结构和赋值时的行为,同时指出了“引用类型”这一术语在Go中的使用可能会引起的误解。

Go内存分布方式

在C中,每个值在内存中只占据一个内存块(一段连续内存);但是,一些Go类型的值可能占据多个内存块。

以后,我们称一个Go值分布在不同内存块上的部分为此值的各个值部(value part)。 一个分布在多个内存块上的值含有一个直接值部和若干被此直接值部引用着的间接值部。

上面的段落描述了两个类别的Go类型。下表将列出这两个类别(category)中的类型(type)种类(kind):

每个值在内存中只分布在一个内存块上的类型

每个值在内存中会分布在多个内存块上的类型

布尔类型

各种数值类型

指针类型

非类型安全指针类型

结构体类型

数组类型

切片类型

映射类型

通道类型

函数类型

接口类型

字符串类型

Go中的两种指针类型

普通的Go指针属于类型安全的指针。事实上,Go还支持另一种称为非类型安全的指针类型。 非类型安全的指针类型提供在unsafe标准库包中。 非类型安全指针类型通常使用unsafe.Pointer来表示。 unsafe.Pointer类似于C语言中的void*。

一个指针值存储着另一个值的地址,除非此指针值是一个nil空指针。 我们可以说此指针引用着另外一个值,或者说另外一个值正被此指针所引用。 一个值可能被间接引用,比如

  • 如果一个结构体值a含有一个指针字段b并且这个指针字段b引用着另外一个值c,那么我们可以说结构体值a也引用着值c。
  • 如果一个值x(直接或者间接地)引用着另一个值y,并且值y(直接或者间接地)引用着第三个值z,则我们可以说值x间接地引用着值z。

将一个含有(直接或者间接)指针字段的结构体类型称为一个指针包裹类型,将一个含有(直接或者间接)指针的类型称为指针持有者类型。 指针类型和指针包裹类型都属于指针持有者类型。元素类型为指针持有者类型的数组类型也是指针持有者类型。

类型内部定义

映射、通道和函数类型的内部定义
// 映射类型
type _map *hashtableImpl // 目前,官方标准编译器是使用
                         // 哈希表来实现映射的。

// 通道类型
type _channel *channelImpl

// 函数类型
type _function *functionImpl

这三个种类的类型的内部结构其实是一个指针类型。 或者说,这些类型的值的直接部分在内部是一个指针。 这些类型的每个值的直接部分引用着它的具体实现的底层间接部分。

切片类型的内部定义

切片类型的内部定义:

type _slice struct{
    elements unsafe.Pointer // 引用着底层的元素
    lenint// 当前的元素个数
    capint// 切片的容量}

从这个定义可以看出来,一个切片类型在内部可以看作是一个指针包裹类型。 每个非零切片值包含着一个底层间接部分用来存储此切片的元素。 一个切片值的底层元素序列(间接部分)被此切片值的elements字段所引用。

字符串类型的内部结构
type _string struct {
    elements *byte // 引用着底层的byte元素
    len      int   // 字符串的长度
}

从此定义可以看出,每个字符串类型在内部也可以看作是一个指针包裹类型。 每个非零字符串值含有一个指针字段 elements。 这个指针字段引用着此字符串值的底层字节元素序列。

接口类型的内部定义

我们可以认为接口类型在内部是如下定义的:

type _interface struct {
    dynamicType  *_type         // 引用着接口值的动态类型
    dynamicValue unsafe.Pointer // 引用着接口值的动态值
}

从这个定义来看,接口类型也可以看作是一个指针包裹类型。一个接口类型含有两个指针字段。 每个非零接口值的(两个)间接部分分别存储着此接口值的动态类型和动态值。 这两个间接部分被此接口值的直接字段dynamicType和dynamicValue所引用。

赋值

底层间接值部将不会被复制

现在我们了解了第二个分类中的类型的内部结构是一个指针持有(指针或者指针包裹)类型。 这对于我们理解Go中的值复制行为有很大帮助。

在Go中,每个赋值操作(包括函数调用传参等)都是一个值的浅复制过程(假设源值和目标值的类型相同)。 换句话说,在一个赋值操作中,只有源值的直接部分被复制给了目标值。 如果源值含有间接部分,则在此赋值操作完成之后,目标值和源值的直接部分将引用着相同的间接部分。

换句话说,两个值将共享底层的间接值部,如下图所示:

事实上,对于字符串值和接口值的赋值,上述描述在理论上并非百分百正确。 官方FAQ明确说明了在一个接口值的赋值中,接口的底层动态值将被复制到目标值。 但是,因为一个接口值的动态值是只读的,所以在接口值的赋值中,官方标准编译器并没有复制底层的动态值。这可以被视为是一个编译器优化。 对于字符串值的赋值,道理是一样的。所以对于官方标准编译器来说,上一段的描述是100%正确的。

因为一个间接值部可能并不专属于任何一个值,所以在使用unsafe.Sizeof函数计算一个值的尺寸的时候,此值的间接部分所占内存空间未被计算在内。

“引用”术语

  • 在Go中,只有切片、映射、通道和函数类型属于引用类型。 (如果我们确实需要引用类型这个术语,那么我们不应把其它指针持有者类型排除在引用类型之外。)
  • 一些函数调用的参数是通过引用来传递的。 (对不起,在Go中,所有的函数调用的参数都是通过值复制直接值部的方式来传递的。)

我并不是想说引用类型这个术语在Go中是完全没有价值的, 我只是想表达这个术语是完全没有必要的,并且它常常在Go的使用中导致一些困惑。

相关文章
|
7月前
|
存储 Go iOS开发
掌握Go语言:探索Go语言指针,解锁高效内存操作与动态数据结构的奥秘(19)
掌握Go语言:探索Go语言指针,解锁高效内存操作与动态数据结构的奥秘(19)
|
25天前
|
编译器 Go
探索 Go 语言中的内存对齐:为什么结构体大小会有所不同?
在 Go 语言中,内存对齐是优化内存访问速度的重要概念。通过调整数据在内存中的位置,编译器确保不同类型的数据能够高效访问。本文通过示例代码展示了两个结构体 `A` 和 `B`,尽管字段相同但排列不同,导致内存占用分别为 40 字节和 48 字节。通过分析内存布局,解释了内存对齐的原因,并提供了优化结构体字段顺序的方法,以减少内存填充,提高性能。
39 3
|
1月前
|
Java 编译器 测试技术
go语言避免不必要的内存分配
【10月更文挑战第18天】
47 1
|
1月前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
28 2
|
7月前
|
算法 Java Go
Go vs Java:内存管理与垃圾回收机制对比
对比了Go和Java的内存管理与垃圾回收机制。Java依赖JVM自动管理内存,使用堆栈内存并采用多种垃圾回收算法,如标记-清除和分代收集。Go则提供更多的手动控制,内存分配与释放由分配器和垃圾回收器协同完成,使用三色标记算法并发回收。示例展示了Java中对象自动创建和销毁,而Go中开发者需注意内存泄漏。选择语言应根据项目需求和技术栈来决定。
|
4月前
|
存储 安全 Go
Go 中的指针:了解内存引用
Go 中的指针:了解内存引用
|
4月前
|
缓存 Java 编译器
Go 中的内存布局和分配原理
Go 中的内存布局和分配原理
|
4月前
|
缓存 编解码 测试技术
使用Go实现健壮的内存型缓存
使用Go实现健壮的内存型缓存
72 2
|
4月前
|
存储 Go
Go 内存分配:结构体中的优化技巧
Go 内存分配:结构体中的优化技巧
|
4月前
|
安全 Java Go
最新进展:Go arena 手动管理内存,鸽了!
最新进展:Go arena 手动管理内存,鸽了!