Go 內(nèi)存對齊-結(jié)構(gòu)體

無論什么語言,類型都涉及到了編程語法的方方面面戚揭。加強對于類型和指針的理解诱告,對于提高編程水平十分關(guān)鍵。本文會主要講解類型。

關(guān)于為什么需要內(nèi)存對齊請看這里:
內(nèi)存管理-內(nèi)存對齊

我們首先來看看這幾個字節(jié)的內(nèi)存:

FFE4 FFE4 FFE4 FFE4
00000000 11001011 01100101 00001010

請問地址 FFE1 上字節(jié)的值是多少精居?如果你試圖回答一個結(jié)果锄禽,那就是錯的。為什么靴姿?因為我還沒有告訴你這個字節(jié)表示什么沃但。我還沒有告訴你類型信息。

如果我說上述字節(jié)表示一個數(shù)字會怎么樣呢佛吓?你可能會回答 10宵晚,那么你又錯了。為什么维雇?因為當(dāng)我說這是數(shù)字的時候淤刃,你認(rèn)為我是指十進(jìn)制的數(shù)字。

基數(shù)(number base):

所有編號系統(tǒng)(numbering system)要發(fā)揮作用吱型,都要有一個基(base)逸贾。從你出生的時候開始,人們就教你用基數(shù) 10 來數(shù)數(shù)了津滞。這可能是因為我們大多數(shù)人都有 10 個手指和 10 個腳趾铝侵。另外,用基數(shù) 10 來進(jìn)行數(shù)學(xué)計算也很自然触徐。

基定義了編號系統(tǒng)所包含的符號數(shù)咪鲜。基數(shù) 10 會有 10 個不同的符號撞鹉,用以表示我們可以計量的無限事物嗜诀。基數(shù) 10 的編號系統(tǒng)為 0孔祸、1、2发皿、3崔慧、4、5穴墅、6惶室、7、8玄货、9皇钞。一旦超過了 9,我們需要增加數(shù)的長度松捉。例如夹界,10、100 和 1000隘世。

在計算機領(lǐng)域可柿,我們還一直使用其他兩種基鸠踪。第一種是基數(shù) 2(或二進(jìn)制數(shù)),例如上圖所表示的位复斥。第二種是基數(shù) 16(或十六進(jìn)制數(shù))营密,例如上圖中表示的地址。

在二進(jìn)制編號系統(tǒng)(基數(shù) 2)中目锭,只有兩種符號评汰,即 0 和 1。

在十六進(jìn)制數(shù)字系統(tǒng)(基數(shù) 16)中痢虹,有 16 個符號被去,這些符號分別是:0、1世分、2编振、3、4臭埋、5踪央、6、7瓢阴、8畅蹂、9、A荣恐、B液斜、C、D叠穆、E少漆、F。

如果桌上有些蘋果硼被,那些蘋果可以用任何編號系統(tǒng)來表示示损。我們可以說這里有:

10010001 個蘋果(使用 2 作為基數(shù))
145 個蘋果(使用 10 作為基數(shù))
91 個蘋果(使用 16 作為基數(shù))
所有答案都正確,只要給定了正確的基嚷硫。

注意每個編號系統(tǒng)表示那些蘋果所需要的符號數(shù)检访。基數(shù)越大仔掸,編號系統(tǒng)的效率就越高脆贵。

對于計算機地址、IP 地址和顏色代碼起暮,使用 16 作為基數(shù)卖氨,就顯得很有價值。

看看用三種基,來分別表示 HTML 的顏色(“白”)的數(shù)字:

使用 2 作為基數(shù):1111 1111 1111 1111 1111 1111(24 個字符)
使用 10 作為基數(shù):16777215(10 個字符)
使用 16 作為基數(shù):FFFFFF(6 個字符)
你會選擇哪個編號系統(tǒng)來表示顏色呢双泪?

現(xiàn)在持搜,如果我告訴你,地址 FFE1 處的字節(jié)表示一個基數(shù)為 10 的數(shù)字焙矛,你回答 10葫盼,這就正確了。

類型提供了兩條信息村斟,你和編譯器都需要它來執(zhí)行我們剛剛經(jīng)歷過的練習(xí)贫导。

要查看的內(nèi)存數(shù)量(以字節(jié)為單位)
這些字節(jié)的表示
Go 語言提供了以下基本數(shù)字類型:

無符號整數(shù)
uint8, uint16, uint32, uint64
有符號整數(shù)
int8, int16, int32, int64
實數(shù)
float32, float64
預(yù)聲明整數(shù)
uint, int, uintptr

這些關(guān)鍵字提供了所有的類型信息。

uint8 包含一個基為 10 的數(shù)字蟆盹,用 1 個存儲字節(jié)表示孩灯。uint8 的值從 0 到 255。

int32 包含一個基為 10 的數(shù)字逾滥,用 4 個存儲字節(jié)表示峰档。int32 的值從 -2147483648 到 2147483647。

預(yù)聲明整數(shù)會根據(jù)你構(gòu)建代碼時的體系結(jié)構(gòu)來進(jìn)行映射寨昙。在 64 位操作系統(tǒng)上讥巡,int 將映射到 int64,而在 32 位系統(tǒng)上舔哪,它將映射到 int32欢顷。

所有存儲在內(nèi)存中的內(nèi)容都解析為某種數(shù)字類型。在 Go 中捉蚤,字符串只是一系列 uint8 類型抬驴,并包含了一些規(guī)則,用于關(guān)聯(lián)這些字節(jié)和識別字符串的結(jié)尾位置缆巧。

在 Go 中布持,指針就是 uintptr 類型。同樣地陕悬,基于操作系統(tǒng)的體系結(jié)構(gòu)题暖,它將映射為 uint32 或者 uint64。Go 為指針創(chuàng)建了一個特殊的類型墩莫。在過去,許多 C 程序員在編寫代碼時逞敷,會認(rèn)為指針值總能符合 unsigned int狂秦。隨著時間的推移,語言和體系結(jié)構(gòu)不斷升級推捐,最終這不再是對的了裂问。由于地址變得比預(yù)先聲明的 unsigned int 更大,很多代碼都出錯了。

結(jié)構(gòu)體類型只是很多類型的組合堪簿,而這些類型也最終會解析為數(shù)字類型痊乾。

type Example struct{
    BoolValue bool
    IntValue  int16
    FloatValue float32
}

該結(jié)構(gòu)體表示一個復(fù)雜類型。它表示 7 個字節(jié)椭更,有三種不同的數(shù)字表示哪审。bool 有 1 個字節(jié),int16 有 2 個字節(jié)虑瀑,而 float32 有 4 個字節(jié)湿滓。但是,這個結(jié)構(gòu)體最終在內(nèi)存中分配了 8 個字節(jié)舌狗。

為了最大限度地減少內(nèi)存碎片整理(memory defragmentation)叽奥,分配內(nèi)存時都會將內(nèi)存邊界對齊。要確定 Go 在體系結(jié)構(gòu)上所用的對齊邊界(alignment boundary)痛侍,你可以運行 unsafe.Alignof 函數(shù)朝氓。Go 在 64 位 Darwin 平臺的對齊邊界是 8 個字節(jié)。因此在 Go 確定我們結(jié)構(gòu)體的內(nèi)存分配時主届,它將填充字節(jié)以確保最終占用的內(nèi)存是 8 的倍數(shù)赵哲。編譯器會決定在哪里添加填充。
下面的程序會顯示對于 Example 結(jié)構(gòu)體類型岂膳,Go 向內(nèi)存所插入的填充:

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

該結(jié)構(gòu)體類型的對齊邊界的確是 8 字節(jié)誓竿。

Size 大小值表示某字段讀寫時所用的內(nèi)存。不出所料谈截,該值與字段的類型信息相一致筷屡。

Offset 偏移值表示字段的開始位置,在內(nèi)存占用中的字節(jié)序號簸喂。

Addr 地址值表示每個字段開始在內(nèi)存占用中所處的位置毙死。

我們可以看到,Go 在 BoolValue 和 IntValue 字段之間填充了 1 個字節(jié)喻鳄。偏移值和兩個地址之差是 2 個字節(jié)扼倘。你還可以看到,下一個內(nèi)存分配時是從結(jié)構(gòu)體最后的字段處分配 4 個字節(jié)除呵。

我們讓結(jié)構(gòu)體只有一個 bool 字段(1 字節(jié))再菊,來證實 8 字節(jié)對齊法則。

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

把兩個地址相減颜曾,你將看到兩種結(jié)構(gòu)體類型分配之間存在 8 個字節(jié)的間隙纠拔。此外,這一次的內(nèi)存分配從上一示例相同的地址開始泛豪。為了保持對齊邊界稠诲,Go 向結(jié)構(gòu)體填充了 7 個字節(jié)侦鹏。

無論如何填充,Size 值實際上表示我們可以為每個字段讀寫的內(nèi)存大小臀叙。

我們只能在使用數(shù)字類型時略水,才能操作內(nèi)存,通過賦值運算符(=)可以做到這一點劝萤。為了方便渊涝,Go 創(chuàng)建了一些可以支持賦值運算符的復(fù)雜類型。這些類型有字符串稳其、數(shù)組和切片驶赏。要查看這些類型的完整列表,請查看此文檔:http://golang.org/ref/spec#Types既鞠。

這些復(fù)雜類型其實對底層數(shù)字類型進(jìn)行了抽象煤傍,我們可以在各種復(fù)雜類型的實現(xiàn)發(fā)現(xiàn)這一點。在這種情況下嘱蛋,這些復(fù)雜類型可以像數(shù)字類型那樣直接讀取內(nèi)存蚯姆。

Go 是一種類型安全的語言。這意味著洒敏,編譯器將始終強制賦值運算符的兩邊類型保持相似龄恋。這非常重要,因為這會防止我們錯誤地讀取內(nèi)存凶伙。

假設(shè)我們想做下面的事郭毕。如果你試圖編譯代碼,你會得到一個錯誤函荣。

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 個字節(jié))的內(nèi)存地址显押,并把它存儲在類型為 int32 的指針上。接下來傻挂,我試圖用指針乘碑,向內(nèi)存地址寫入一個 4 個字節(jié)的整數(shù)。如果可以使用該指針金拒,那么我就會違反 IntValue 字段的類型規(guī)則兽肤,并在此過程中破壞內(nèi)存。

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

根據(jù)上面的內(nèi)存占用情況绪抛,指針將在 FFE3 和 FFE6 之間的 4 個字節(jié)中寫入 20资铡。IntValue 的值將如預(yù)期的那樣變?yōu)?20,但 FloatValue 的值現(xiàn)在等于 0幢码。想象一下笤休,寫入這些字節(jié)超出了該結(jié)構(gòu)體的內(nèi)存分配,并且開始破壞應(yīng)用的其他區(qū)域的內(nèi)存蛤育。隨之而來的錯誤會是隨機宛官、不可預(yù)測的。

Go 編譯器會一直保證內(nèi)存對齊和轉(zhuǎn)型是安全的瓦糕。

在下面一個轉(zhuǎn)型的示例中底洗,編譯器會報錯:

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)
}

首先,我們在系統(tǒng)中新建了一個 int32Ext 類型咕娄,并告訴編譯器該類型表示一個 int32亥揖。接下來,我們創(chuàng)建了一個名為 jill 的新變量圣勒,將其賦值為 10费变。編譯器允許這個賦值操作,因為數(shù)字類型在賦值運算符的右側(cè)圣贸。編譯器知道賦值是安全的挚歧。

現(xiàn)在,我們嘗試創(chuàng)建第二個變量吁峻,名為 jack滑负,其類型為 int32,我們將 jill 賦值給 jack用含。在這里矮慕,編譯器會拋出錯誤:

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

編譯器認(rèn)為 jill 的類型是 int32Ext,不會對賦值的安全性作出任何假設(shè)啄骇。

現(xiàn)在我們使用強制轉(zhuǎn)換痴鳄,編譯器允許賦值,并如預(yù)期打印出值來缸夹。當(dāng)我們執(zhí)行轉(zhuǎn)型時痪寻,編譯器會檢查賦值的安全性。在這里明未,編譯器確定了這是相同類型的值槽华,于是允許賦值操作。

對于某些讀者來說趟妥,這似乎很基礎(chǔ)猫态,但它是使用任何編程語言的基石。即使類型是經(jīng)過抽象的披摄,你也是在操作內(nèi)存亲雪,你應(yīng)該知道你究竟在做些什么。

有了這些基礎(chǔ)疚膊,我們才可以在 Go 中討論指針义辕,然后將參數(shù)傳遞給函數(shù)。

像往常一樣寓盗,我希望這篇文章灌砖,能夠幫助你了解一些可能存在的盲區(qū)璧函。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市基显,隨后出現(xiàn)的幾起案子蘸吓,更是在濱河造成了極大的恐慌,老刑警劉巖撩幽,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件库继,死亡現(xiàn)場離奇詭異,居然都是意外死亡窜醉,警方通過查閱死者的電腦和手機宪萄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來榨惰,“玉大人拜英,你說我怎么就攤上這事±糯撸” “怎么了聊记?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長恢暖。 經(jīng)常有香客問我排监,道長,這世上最難降的妖魔是什么杰捂? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任舆床,我火速辦了婚禮,結(jié)果婚禮上嫁佳,老公的妹妹穿的比我還像新娘挨队。我一直安慰自己,他們只是感情好蒿往,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布盛垦。 她就那樣靜靜地躺著,像睡著了一般瓤漏。 火紅的嫁衣襯著肌膚如雪腾夯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天蔬充,我揣著相機與錄音蝶俱,去河邊找鬼。 笑死饥漫,一個胖子當(dāng)著我的面吹牛榨呆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庸队,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼积蜻,長吁一口氣:“原來是場噩夢啊……” “哼闯割!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起竿拆,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤纽谒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后如输,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡央勒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年不见,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崔步。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡稳吮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出井濒,到底是詐尸還是另有隱情灶似,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布瑞你,位于F島的核電站酪惭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏者甲。R本人自食惡果不足惜春感,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望虏缸。 院中可真熱鬧鲫懒,春花似錦、人聲如沸刽辙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宰缤。三九已至颂翼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間慨灭,已是汗流浹背疚鲤。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留缘挑,地道東北人集歇。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像语淘,于是被迫代替她去往敵國和親诲宇。 傳聞我的和親對象是個殘疾皇子际歼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容