當我使用 C/C++ 編寫代碼時,理解類型(type)是非常有必要的。如果不理解類型,你就會在編譯或者運行代碼的時候,碰到一大堆麻煩。無論什麼語言,類型都涉及到了編程文法的方方面面。加強對於類型和指標的理解,對於提高編程水平十分關鍵。本文會主要講解類型。我們首先來看看這幾個位元組的記憶體:FFE4 | FFE3 | FFE2 | FFE1---|---|---|---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,這就正確了。類型提供了兩條資訊,你和編譯器都需要它來執行我們剛剛經曆過的練習。1. 要查看的記憶體數量(以位元組為單位)2. 這些位元組的表示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` 更大,很多代碼都出錯了。結構體類型只是很多類型的組合,而這些類型也最終會解析為數字類型。```gotype 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 的倍數。編譯器會決定在哪裡添加填充。如果你想要學習更多有關結構體成員對齊和填充的知識,請查看下面的連結:http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/下面的程式會顯示對於 `Example` 結構體類型,Go 向記憶體所插入的填充:```gopackage mainimport ( "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)}```輸出如下所示:```bashAlignment Boundary: 8BoolValue = Size: 1 Offset: 0 Addr: 0x21015b018IntValue = Size: 2 Offset: 2 Addr: 0x21015b01aFloatValue = Size: 4 Offset: 4 Addr: 0x21015b01cNext = Size: 1 Offset: 0 Addr: 0x21015b020```該結構體類型的對齊邊界的確是 8 位元組。`Size` 大小值表示某欄位讀寫時所用的記憶體。不出所料,該值與欄位的類型資訊相一致。`Offset` 位移值表示欄位的開始位置,在記憶體佔用中的位元組序號。`Addr` 地址值表示每個欄位開始在記憶體佔用中所處的位置。我們可以看到,Go 在 `BoolValue` 和 `IntValue` 欄位之間填充了 1 個位元組。位移值和兩個地址之差是 2 個位元組。你還可以看到,下一個記憶體配置時是從結構體最後的欄位處分配 4 個位元組。我們讓結構體只有一個 `bool` 欄位(1 位元組),來證實 8 位元組對齊法則。```gopackage mainimport ( "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)}```其輸出如下:```bashAlignment Boundary: 8BoolValue = Size: 1 Offset: 0 Addr: 0x21015b018Next = Size: 1 Offset: 0 Addr: 0x21015b020```把兩個地址相減,你將看到兩種結構體類型分配之間存在 8 個位元組的間隙。此外,這一次的記憶體配置從上一樣本相同的地址開始。為了保持對齊邊界,Go 向結構體填充了 7 個位元組。無論如何填充,`Size` 值實際上表示我們可以為每個欄位讀寫的記憶體大小。我們只能在使用數字類型時,才能操作記憶體,通過賦值運算子(=)可以做到這一點。為了方便,Go 建立了一些可以支援賦值運算子的複雜類型。這些類型有字串、數組和切片。要查看這些類型的完整列表,請查看此文檔:http://golang.org/ref/spec#Types。這些複雜類型其實對底層數字類型進行了抽象,我們可以在各種複雜類型的實現發現這一點。在這種情況下,這些複雜類型可以像數字類型那樣直接讀取記憶體。Go 是一種型別安全的語言。這意味著,編譯器將始終強制賦值運算子的兩邊類型保持相似。這非常重要,因為這會防止我們錯誤地讀取記憶體。假設我們想做下面的事。如果你試圖編譯代碼,你會得到一個錯誤。```gotype Example struct{ BoolValue bool IntValue int16 FloatValue float32}example := &Example{ BoolValue: true, IntValue: 10, FloatValue: 3.141592,}var pointer *int32pointer = *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 | truepointer |---|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 編譯器會一直保證記憶體對齊和轉型是安全的**。**在下面一個轉型的樣本中,編譯器會報錯**:```goackage mainimport ( "fmt")// Create a new typetype int32Ext int32func 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`。在這裡,編譯器會拋出錯誤:```bashcannot use jill (type int32Ext) as type int32 in assignment```編譯器認為 `jill` 的類型是 `int32Ext`,不會對賦值的安全性作出任何假設。現在我們使用強制轉換,編譯器允許賦值,並如預期列印出值來。當我們執行轉型時,編譯器會檢查賦值的安全性。在這裡,編譯器確定了這是相同類型的值,於是允許賦值操作。對於某些讀者來說,這似乎很基礎,但它是使用任何程式設計語言的基石。即使類型是經過抽象的,你也是在操作記憶體,你應該知道你究竟在做些什麼。有了這些基礎,我們才可以在 Go 中討論指標,然後將參數傳遞給函數。像往常一樣,我希望這篇文章,能夠協助你瞭解一些可能存在的盲區。
via: https://www.ardanlabs.com/blog/2013/07/understanding-type-in-go.html
作者:William Kennedy 譯者:Noluye 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
432 次點擊