深入理解 Go 語言的類型
當(dāng)我使用 C/C++ 編寫代碼時,理解類型(type)是非常有必要的。如果不理解類型,你就會在編譯或者運行代碼的時候,碰到一大堆麻煩。無論什么語言,類型都涉及到了編程語法的方方面面。加強(qiáng)對于類型和指針的理解,對于提高編程水平十分關(guān)鍵。本文會主要講解類型。
我們首先來看看這幾個字節(jié)的內(nèi)存:
| FFE4 | FFE3 | FFE2 | FFE1 |
|---|---|---|---|
| 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。
在計算機(jī)領(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)的效率就越高。
對于計算機(jī)地址、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ù)。編譯器會決定在哪里添加填充。
如果你想要學(xué)習(xí)更多有關(guān)結(jié)構(gòu)體成員對齊和填充的知識,請查看下面的鏈接:
http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/
下面的程序會顯示對于 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 是一種類型安全的語言。這意味著,編譯器將始終強(qiáng)制賦值運算符的兩邊類型保持相似。這非常重要,因為這會防止我們錯誤地讀取內(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)存。隨之而來的錯誤會是隨機(jī)、不可預(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)在我們使用強(qiáng)制轉(zhuǎn)換,編譯器允許賦值,并如預(yù)期打印出值來。當(dāng)我們執(zhí)行轉(zhuǎn)型時,編譯器會檢查賦值的安全性。在這里,編譯器確定了這是相同類型的值,于是允許賦值操作。
對于某些讀者來說,這似乎很基礎(chǔ),但它是使用任何編程語言的基石。即使類型是經(jīng)過抽象的,你也是在操作內(nèi)存,你應(yīng)該知道你究竟在做些什么。
有了這些基礎(chǔ),我們才可以在 Go 中討論指針,然后將參數(shù)傳遞給函數(shù)。
像往常一樣,我希望這篇文章,能夠幫助你了解一些可能存在的盲區(qū)。
via: https://www.ardanlabs.com/blog/2013/07/understanding-type-in-go.html
作者:William Kennedy[1]譯者:Noluye[2]校對:polaris1119[3]
本文由 GCTT[4] 原創(chuàng)編譯,Go 中文網(wǎng)[5] 榮譽(yù)推出
參考資料
William Kennedy: https://github.com/ardanlabs/gotraining
[2]Noluye: https://github.com/Noluye
[3]polaris1119: https://github.com/polaris1119
[4]GCTT: https://github.com/studygolang/GCTT
[5]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
