Go 内存对齐-结构体

无论什么语言,类型都涉及到了编程语法的方方面面。加强对于类型和指针的理解,对于提高编程水平十分关键。本文会主要讲解类型。

关于为什么需要内存对齐请看这里:
内存管理-内存对齐

我们首先来看看这几个字节的内存:

FFE4 FFE4 FFE4 FFE4
00000000 11001011 01100101 00001010

请问地址 FFE1 上字节的值是多少?如果你试图回答一个结果,那就是错的。为什么?因为我还没有告诉你这个字节表示什么。我还没有告诉你类型信息。

如果我说上述字节表示一个数字会怎么样呢?你可能会回答 10,那么你又错了。为什么?因为当我说这是数字的时候,你认为我是指十进制的数字。

基数(number base):

所有编号系统(numbering system)要发挥作用,都要有一个基(base)。从你出生的时候开始,人们就教你用基数 10 来数数了。这可能是因为我们大多数人都有 10 个手指和 10 个脚趾。另外,用基数 10 来进行数学计算也很自然。

基定义了编号系统所包含的符号数。基数 10 会有 10 个不同的符号,用以表示我们可以计量的无限事物。基数 10 的编号系统为 0、1、2、3、4、5、6、7、8、9。一旦超过了 9,我们需要增加数的长度。例如,10、100 和 1000。

在计算机领域,我们还一直使用其他两种基。第一种是基数 2(或二进制数),例如上图所表示的位。第二种是基数 16(或十六进制数),例如上图中表示的地址。

在二进制编号系统(基数 2)中,只有两种符号,即 0 和 1。

在十六进制数字系统(基数 16)中,有 16 个符号,这些符号分别是:0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F。

如果桌上有些苹果,那些苹果可以用任何编号系统来表示。我们可以说这里有:

10010001 个苹果(使用 2 作为基数)
145 个苹果(使用 10 作为基数)
91 个苹果(使用 16 作为基数)
所有答案都正确,只要给定了正确的基。

注意每个编号系统表示那些苹果所需要的符号数。基数越大,编号系统的效率就越高。

对于计算机地址、IP 地址和颜色代码,使用 16 作为基数,就显得很有价值。

看看用三种基,来分别表示 HTML 的颜色(“白”)的数字:

使用 2 作为基数:1111 1111 1111 1111 1111 1111(24 个字符)
使用 10 作为基数:16777215(10 个字符)
使用 16 作为基数:FFFFFF(6 个字符)
你会选择哪个编号系统来表示颜色呢?

现在,如果我告诉你,地址 FFE1 处的字节表示一个基数为 10 的数字,你回答 10,这就正确了。

类型提供了两条信息,你和编译器都需要它来执行我们刚刚经历过的练习。

要查看的内存数量(以字节为单位)
这些字节的表示
Go 语言提供了以下基本数字类型:

无符号整数
uint8, uint16, uint32, uint64
有符号整数
int8, int16, int32, int64
实数
float32, float64
预声明整数
uint, int, uintptr

这些关键字提供了所有的类型信息。

uint8 包含一个基为 10 的数字,用 1 个存储字节表示。uint8 的值从 0 到 255。

int32 包含一个基为 10 的数字,用 4 个存储字节表示。int32 的值从 -2147483648 到 2147483647。

预声明整数会根据你构建代码时的体系结构来进行映射。在 64 位操作系统上,int 将映射到 int64,而在 32 位系统上,它将映射到 int32。

所有存储在内存中的内容都解析为某种数字类型。在 Go 中,字符串只是一系列 uint8 类型,并包含了一些规则,用于关联这些字节和识别字符串的结尾位置。

在 Go 中,指针就是 uintptr 类型。同样地,基于操作系统的体系结构,它将映射为 uint32 或者 uint64。Go 为指针创建了一个特殊的类型。在过去,许多 C 程序员在编写代码时,会认为指针值总能符合 unsigned int。随着时间的推移,语言和体系结构不断升级,最终这不再是对的了。由于地址变得比预先声明的 unsigned int 更大,很多代码都出错了。

结构体类型只是很多类型的组合,而这些类型也最终会解析为数字类型。

type Example struct{
    BoolValue bool
    IntValue  int16
    FloatValue float32
}

该结构体表示一个复杂类型。它表示 7 个字节,有三种不同的数字表示。bool 有 1 个字节,int16 有 2 个字节,而 float32 有 4 个字节。但是,这个结构体最终在内存中分配了 8 个字节。

为了最大限度地减少内存碎片整理(memory defragmentation),分配内存时都会将内存边界对齐。要确定 Go 在体系结构上所用的对齐边界(alignment boundary),你可以运行 unsafe.Alignof 函数。Go 在 64 位 Darwin 平台的对齐边界是 8 个字节。因此在 Go 确定我们结构体的内存分配时,它将填充字节以确保最终占用的内存是 8 的倍数。编译器会决定在哪里添加填充。
下面的程序会显示对于 Example 结构体类型,Go 向内存所插入的填充:

package main

import (
    "fmt"
    "unsafe"
)

type Example struct {
    BoolValue bool
    IntValue int16
    FloatValue float32
}

func main() {
    example := &Example{
        BoolValue:  true,
        IntValue:   10,
        FloatValue: 3.141592,
    }

    exampleNext := &Example{
        BoolValue:  true,
        IntValue:   10,
        FloatValue: 3.141592,
    }

    alignmentBoundary := unsafe.Alignof(example)

    sizeBool := unsafe.Sizeof(example.BoolValue)
    offsetBool := unsafe.Offsetof(example.BoolValue)

    sizeInt := unsafe.Sizeof(example.IntValue)
    offsetInt := unsafe.Offsetof(example.IntValue)

    sizeFloat := unsafe.Sizeof(example.FloatValue)
    offsetFloat := unsafe.Offsetof(example.FloatValue)

    sizeBoolNext := unsafe.Sizeof(exampleNext.BoolValue)
    offsetBoolNext := unsafe.Offsetof(exampleNext.BoolValue)

    fmt.Printf("Alignment Boundary: %d\n", alignmentBoundary)

    fmt.Printf("BoolValue = Size: %d Offset: %d Addr: %v\n",
        sizeBool, offsetBool, &example.BoolValue)

    fmt.Printf("IntValue = Size: %d Offset: %d Addr: %v\n",
        sizeInt, offsetInt, &example.IntValue)

    fmt.Printf("FloatValue = Size: %d Offset: %d Addr: %v\n",
        sizeFloat, offsetFloat, &example.FloatValue)

    fmt.Printf("Next = Size: %d Offset: %d Addr: %v\n",
        sizeBoolNext, offsetBoolNext, &exampleNext.BoolValue)
}

输出如下所示:

Alignment Boundary: 8
BoolValue  = Size: 1  Offset: 0  Addr: 0x21015b018
IntValue   = Size: 2  Offset: 2  Addr: 0x21015b01a
FloatValue = Size: 4  Offset: 4  Addr: 0x21015b01c
Next       = Size: 1  Offset: 0  Addr: 0x21015b020

该结构体类型的对齐边界的确是 8 字节。

Size 大小值表示某字段读写时所用的内存。不出所料,该值与字段的类型信息相一致。

Offset 偏移值表示字段的开始位置,在内存占用中的字节序号。

Addr 地址值表示每个字段开始在内存占用中所处的位置。

我们可以看到,Go 在 BoolValue 和 IntValue 字段之间填充了 1 个字节。偏移值和两个地址之差是 2 个字节。你还可以看到,下一个内存分配时是从结构体最后的字段处分配 4 个字节。

我们让结构体只有一个 bool 字段(1 字节),来证实 8 字节对齐法则。

package main

import (
    "fmt"
    "unsafe"
)

type Example struct {
    BoolValue bool
}

func main() {
    example := &Example{
        BoolValue:  true,
    }

    exampleNext := &Example{
        BoolValue:  true,
    }

    alignmentBoundary := unsafe.Alignof(example)

    sizeBool := unsafe.Sizeof(example.BoolValue)
    offsetBool := unsafe.Offsetof(example.BoolValue)

    sizeBoolNext := unsafe.Sizeof(exampleNext.BoolValue)
    offsetBoolNext := unsafe.Offsetof(exampleNext.BoolValue)

    fmt.Printf("Alignment Boundary: %d\n", alignmentBoundary)

    fmt.Printf("BoolValue = Size: %d Offset: %d Addr: %v\n",
        sizeBool, offsetBool, &example.BoolValue)

    fmt.Printf("Next = Size: %d Offset: %d Addr: %v\n",
        sizeBoolNext, offsetBoolNext, &exampleNext.BoolValue)
}

其输出如下:

Alignment Boundary: 8
BoolValue = Size: 1 Offset: 0 Addr: 0x21015b018
Next      = Size: 1 Offset: 0 Addr: 0x21015b020

把两个地址相减,你将看到两种结构体类型分配之间存在 8 个字节的间隙。此外,这一次的内存分配从上一示例相同的地址开始。为了保持对齐边界,Go 向结构体填充了 7 个字节。

无论如何填充,Size 值实际上表示我们可以为每个字段读写的内存大小。

我们只能在使用数字类型时,才能操作内存,通过赋值运算符(=)可以做到这一点。为了方便,Go 创建了一些可以支持赋值运算符的复杂类型。这些类型有字符串、数组和切片。要查看这些类型的完整列表,请查看此文档:http://golang.org/ref/spec#Types。

这些复杂类型其实对底层数字类型进行了抽象,我们可以在各种复杂类型的实现发现这一点。在这种情况下,这些复杂类型可以像数字类型那样直接读取内存。

Go 是一种类型安全的语言。这意味着,编译器将始终强制赋值运算符的两边类型保持相似。这非常重要,因为这会防止我们错误地读取内存。

假设我们想做下面的事。如果你试图编译代码,你会得到一个错误。

type Example struct{
    BoolValue bool
    IntValue  int16
    FloatValue float32
}

example := &Example{
    BoolValue:  true,
    IntValue:   10,
    FloatValue: 3.141592,
}

var pointer *int32
pointer = *int32(&example.IntValue)
*pointer = 20

我试图获取 IntValue 字段(2 个字节)的内存地址,并把它存储在类型为 int32 的指针上。接下来,我试图用指针,向内存地址写入一个 4 个字节的整数。如果可以使用该指针,那么我就会违反 IntValue 字段的类型规则,并在此过程中破坏内存。

FFE8 FFE7 FFE6 FFE5 FFE4 FFE3 FFE2 FFE1
0 0 0 3.14 0 10 0 true

pointer


FFE3

FFE8 FFE7 FFE6 FFE5 FFE4 FFE3 FFE2 FFE1
0 0 0 0 0 20 0 true

根据上面的内存占用情况,指针将在 FFE3 和 FFE6 之间的 4 个字节中写入 20。IntValue 的值将如预期的那样变为 20,但 FloatValue 的值现在等于 0。想象一下,写入这些字节超出了该结构体的内存分配,并且开始破坏应用的其他区域的内存。随之而来的错误会是随机、不可预测的。

Go 编译器会一直保证内存对齐和转型是安全的。

在下面一个转型的示例中,编译器会报错:

ackage main

import (
    "fmt"
)

// Create a new type
type int32Ext int32

func main() {
    // Cast the number 10 to a value of type Jill
    var jill int32Ext = 10

    // Assign the value of jill to jack
    // ** cannot use jill (type int32Ext) as type int32 in assignment **
    var jack int32 = jill

    // Assign the value of jill to jack by casting
    // ** the compiler is happy **
    var jack int32 = int32(jill)

    fmt.Printf("%d\n", jack)
}

首先,我们在系统中新建了一个 int32Ext 类型,并告诉编译器该类型表示一个 int32。接下来,我们创建了一个名为 jill 的新变量,将其赋值为 10。编译器允许这个赋值操作,因为数字类型在赋值运算符的右侧。编译器知道赋值是安全的。

现在,我们尝试创建第二个变量,名为 jack,其类型为 int32,我们将 jill 赋值给 jack。在这里,编译器会抛出错误:

cannot use jill (type int32Ext) as type int32 in assignment

编译器认为 jill 的类型是 int32Ext,不会对赋值的安全性作出任何假设。

现在我们使用强制转换,编译器允许赋值,并如预期打印出值来。当我们执行转型时,编译器会检查赋值的安全性。在这里,编译器确定了这是相同类型的值,于是允许赋值操作。

对于某些读者来说,这似乎很基础,但它是使用任何编程语言的基石。即使类型是经过抽象的,你也是在操作内存,你应该知道你究竟在做些什么。

有了这些基础,我们才可以在 Go 中讨论指针,然后将参数传递给函数。

像往常一样,我希望这篇文章,能够帮助你了解一些可能存在的盲区。

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

推荐阅读更多精彩内容