詳解Go變量類型的內存布局


定義

每當我們編寫任何程序時,我們都需要在內存中存儲一些數據/信息。數據存儲在特定地址的存儲器中。內存地址看起來像0xAFFFF(這是內存地址的十六進制表示)。

現在,要訪問數據,我們需要知道存儲它的地址。我們可以跟蹤存儲與程序相關的數據的所有內存地址。但想象一下,記住所有內存地址並使用它們訪問數據會有非常困難。這就是為什么引入變量。

變量是一種占位符,用於引用計算機的內存地址,可理解為內存地址的標簽。

 

什么是指針

指針是存儲另一個變量的內存地址的變量。所以指針也是一種變量,只不過它是一種特殊變量,它的值存放的是另一個變量的內存地址。

在上面的例子中,指針p包含值0x0001,該值是變量的地址a

 

 

 

 

 

 

類型占用內存情況

unsafe包可以獲取變量的內存使用情況

Go語言提供以下基本數字類型:

無符號整數 uint8,uint16,uint32,uint64

符號整數 int8,int16,int32,int64

實數 float32,float64 Predeclared

整數(和平台相關) uint,int,uintptr (指針)

32位系統

uint=uint32 int=int32 uintptr(指針)為32位的指針

64位系統

uint=uint64 int=int64 uintptr(指針)為64位的指針

Mac OS(64-Bit)示例:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var intValue int
    var uint8Value uint8
    var uint16Value uint16
    var uint32Value uint32
    var uint64Value uint64
    var int8Value int8
    var int16Value int16
    var int32Value int32
    var int64Value int64

    var float32Value float32
    var float64Value float64
    var boolValue bool
    var ptrValue uintptr
    var complex64Value complex64
    var complex128Value complex128
    var strValue string
    var byteValue byte
    var runeValue rune
    structValue := struct {
        FieldA float32
        FieldB string
    }{0, ""}
    mapValue := map[int]int{}
    var sliceValue []int
    var intPtrValue *int
    var chanValue chan int
    var funcValue func()


    fmt.Println("funcValue = Size:", unsafe.Sizeof(funcValue) ) //size:  8
    fmt.Println("chanValue = Size:", unsafe.Sizeof(chanValue) ) //size:  8
    fmt.Println("intPtrValue = Size:", unsafe.Sizeof(intPtrValue) ) //size:  8
    fmt.Println("sliceValue = Size:", unsafe.Sizeof(sliceValue) ) //size:  24
    //type slice struct {
    //    array unsafe.Pointer
    //    len   int
    //    cap   int
    //}
    fmt.Println("mapValue = Size:", unsafe.Sizeof(mapValue) ) //size:  8
    fmt.Println("structValue = Size:", unsafe.Sizeof(structValue) ) //size:  24
    fmt.Println("strValue = Size:", unsafe.Sizeof(strValue), len(strValue)) //intValue = Size: 16, string is the set of all strings of 8-bit bytes, conventionally but not
    // necessarily representing UTF-8-encoded text. A string may be empty, but
    // not nil. Values of string type are immutable.
    //type stringStruct struct {
    //    str unsafe.Pointer
    //    len int
    //}
    fmt.Println("byteValue = Size:", unsafe.Sizeof(byteValue)) //intValue = Size: 1
    fmt.Println("runeValue = Size:", unsafe.Sizeof(runeValue)) //intValue = Size: 4
    fmt.Println("boolValue = Size:", unsafe.Sizeof(boolValue)) //intValue = Size: 1
    fmt.Println("ptrValue = Size:", unsafe.Sizeof(ptrValue)) //intValue = Size: 8
    fmt.Println("complex64Value = Size:", unsafe.Sizeof(complex64Value)) //intValue = Size: 8
    fmt.Println("complex128Value = Size:", unsafe.Sizeof(complex128Value)) //intValue = Size: 16
    fmt.Println("intValue = Size:", unsafe.Sizeof(intValue)) //intValue = Size: 8
    fmt.Println("uint8Value = Size:", unsafe.Sizeof(uint8Value)) //uint8Value = Size: 1
    fmt.Println("uint16Value = Size:", unsafe.Sizeof(uint16Value)) //uint16Value = Size: 2
    fmt.Println("uint32Value = Size:", unsafe.Sizeof(uint32Value)) //uint32Value = Size: 4
    fmt.Println("uint64Value = Size:", unsafe.Sizeof(uint64Value))// uint64Value = Size: 8

    fmt.Println("int8Value = Size:", unsafe.Sizeof(int8Value)) //int8Value = Size: 1
    fmt.Println("int16Value = Size:", unsafe.Sizeof(int16Value))//int16Value = Size: 2
    fmt.Println("int32Value = Size:", unsafe.Sizeof(int32Value))//int32Value = Size: 4
    fmt.Println("int64Value = Size:", unsafe.Sizeof(int64Value)) //int64Value = Size: 8

    fmt.Println("float32Value = Size:", unsafe.Sizeof(float32Value)) //float32Value = Size: 4
    fmt.Println("float64Value = Size:", unsafe.Sizeof(float64Value))//float64Value = Size: 8

}
# go run size.go
funcValue = Size: 8
chanValue = Size: 8
intPtrValue = Size: 8
sliceValue = Size: 24
mapValue = Size: 8
structValue = Size: 24
strValue = Size: 16 0
byteValue = Size: 1
runeValue = Size: 4
boolValue = Size: 1
ptrValue = Size: 8
complex64Value = Size: 8
complex128Value = Size: 16
intValue = Size: 8
uint8Value = Size: 1
uint16Value = Size: 2
uint32Value = Size: 4
uint64Value = Size: 8
int8Value = Size: 1
int16Value = Size: 2
int32Value = Size: 4
int64Value = Size: 8
float32Value = Size: 4
float64Value = Size: 8

 

上面的是基本類型,接下來了解下復雜類型,以結構體類型為例

type Example struct {
   BoolValue  bool
   IntValue   int16
   FloatValue float32
}

 

該結構代表復雜類型。它代表7個字節,帶有三個不同的數字表示。bool是一個字節,int16是2個字節,float32增加4個字節。但是,在此結構的內存中實際分配了8個字節。

所有內存都分配在對齊邊界上,以最大限度地減少內存碎片整理。要確定對齊邊界Go用於您的體系結構,您可以運行unsafe.Alignof函數。Go為64bit Darwin平台的對齊邊界是8個字節。因此,當Go確定結構的內存分配時,它將填充字節以確保最終內存占用量是8的倍數。編譯器將確定添加填充的位置。

什么是內存對齊呢?

內存對齊,也叫邊界對齊(boundary alignment),是處理器為了提高處理性能而對存取數據的起始地址所提出的一種要求。編譯器為了使我們編寫的C程序更有效,就必須最大限度地滿足處理器對邊界對齊的要求。

從處理器的角度來看,需要盡可能減少對內存的訪問次數以實現對數據結構進行更加高效的操作。為什么呢?因為盡管處理器包含了緩存,但它在處理數據時還得讀取緩存中的數據,讀取緩存的次數當然是越少越好!如上圖所示,在采用邊界對齊的情況下,當處理器需要訪問a_變量和b_變量時都只需進行一次存取(圖中花括號表示一次存取操作)。若不采用邊界對齊,a_變量只要一次處理器操作,而b_變量卻至少要進行兩次操作。對於b_,處理器還得調用更多指令將其合成一個完整的4字節,這樣無疑大大降低了程序效率。

以下程序顯示Go插入到Example類型struct的內存占用中的填充:

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("example Size: %d\n", unsafe.Sizeof(example))

    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)

}

 

# go run alignment.go
example Size: 8
Alignment Boundary: 8
BoolValue = Size: 1 Offset: 0 Addr: 0xc000088000
IntValue = Size: 2 Offset: 2 Addr: 0xc000088002
FloatValue = Size: 4 Offset: 4 Addr: 0xc000088004
Next = Size: 1 Offset: 0 Addr: 0xc000088008

 

類型結構的對齊邊界是預期的8個字節。

大小值顯示將讀取和寫入該字段的內存量。正如所料,大小與類型信息一致。

偏移值顯示進入內存占用的字節數,我們將找到該字段的開頭。

地址是可以找到內存占用內每個字段的開頭的地方。

我們可以看到Go在BoolValue和IntValue字段之間填充1個字節。偏移值和兩個地址之間的差異是2個字節。您還可以看到下一個內存分配是從結構中的最后一個字段開始4個字節。

指針的使用

聲明一個指針

使用以下語法聲明類型為T的指針

var p *int

指針的零值nil。這意味着任何未初始化的指針都將具有該值nil。讓我們看一個完整的例子

package main
import "fmt" func main() { var p *int &p=1 }

注意:當指針沒有指向的時候,不能對(*point)進行操作包括讀取,否則會報空指針異常。

示例:

package main

func main() {
   var p *int

   *p = 1 //panic: runtime error: invalid memory address or nil pointer dereference

}

解決方法即給該指針分配一個指向,即初始化一個內存,並把該內存地址賦予指針變量

示例:

import "fmt"

func main() {
   var p *int
   var m int
   p = &m
   *p = 1
   fmt.Println("m=", m)
   fmt.Println("p=", p)
}

或還可以使用內置new()函數創建指針。該new()函數將類型作為參數,分配足夠的內存以容納該類型的值,並返回指向它的指針。

import "fmt"

func main() {
   var p *int

   p = new(int)
   *p = 1
   fmt.Println("p=", *p)
}

初始化指針

您可以使用另一個變量的內存地址初始化指針。可以使用&運算符檢索變量的地址

var x = 100 var p *int = &x

注意我們如何使用&帶變量的運算符x來獲取其地址,然后將地址分配給指針p

就像Golang中的任何其他變量一樣,指針變量的類型也由編譯器推斷。所以你可以省略p上面例子中指針的類型聲明,並像這樣寫

var p = &a

取消引用指針

您可以*在指針上使用運算符來訪問存儲在指針所指向的變量中的值。這被稱為解除引用間接

 

 

package main
import "fmt" func main() { var a = 100 var p = &a fmt.Println("a = ", a) fmt.Println("p = ", p) fmt.Println("*p = ", *p) }

輸出:

a =  100
p =  0xc00004c080
*p =  100

您不僅可以使用*運算符訪問指向變量的值,還可以更改它。以下示例a通過指針設置存儲在變量中的值p

package main
import "fmt" func main() { var a = 1000 var p = &a fmt.Println("a (before) = ", a) // Changing the value stored in the pointed variable through the pointer *p = 2000 fmt.Println("a (after) = ", a) }

輸出:

a (before) =  1000
a (after) =  2000

指針指向指針

指針可以指向任何類型的變量。它也可以指向另一個指針。以下示例顯示如何創建指向另一個指針的指針

package main
import "fmt" func main() { var a = 7.98 var p = &a var pp = &p fmt.Println("a = ", a) fmt.Println("address of a = ", &a) fmt.Println("p = ", p) fmt.Println("address of p = ", &p) fmt.Println("pp = ", pp) // Dereferencing a pointer to pointer fmt.Println("*pp = ", *pp) fmt.Println("**pp = ", **pp) }

Go中沒有指針算術

如果您使用過C / C ++,那么您必須意識到這些語言支持指針算法。例如,您可以遞增/遞減指針以移動到下一個/上一個內存地址。您可以向/從指針添加或減去整數值。您也可以使用關系運算符比較兩個三分球==<>等。

但Go不支持對指針進行此類算術運算。任何此類操作都將導致編譯時錯誤

package main

func main() { var x = 67 var p = &x var p1 = p + 1 // Compiler Error: invalid operation }

但是,您可以使用==運算符比較相同類型的兩個指針的相等性。

package main
import "fmt" func main() { var a = 75 var p1 = &a var p2 = &a if p1 == p2 { fmt.Println("Both pointers p1 and p2 point to the same variable.") } }

Go中傳遞簡單類型

import "fmt"

func main() {
   p := 5
   change(&p)
   fmt.Println("p=", p)//p= 0
}
func change(p *int) {
   *p = 0
}

Go中所有的都是按值傳遞,對於復雜類型,傳的是指針的拷貝

package main

import "fmt"

func main() {
	var m map[string]int
	m = map[string]int{"one": 1, "two": 2}
	n := m
	fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m) // map[two:2 one:1] fmt.Println(n) //map[one:1 two:2] changeMap(m) fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m) //map[one:1 two:2 three:3] fmt.Println(n) //map[one:1 two:2 three:3] } func changeMap(m map[string]int) { m["three"] = 3 fmt.Printf("changeMap func %p\n", m) //changeMap func 0xc000060240 } 

直接傳指針 也是傳指針的拷貝

package main

import "fmt"

func main() {
	var m map[string]int
	m = map[string]int{"one": 1, "two": 2}
	n := m
	fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m) // map[two:2 one:1] fmt.Println(n) //map[one:1 two:2] changeMap(&m) fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m) //map[one:1 two:2 three:3] fmt.Println(n) //map[two:2 three:3 one:1] } func changeMap(m *map[string]int) { //m["three"] = 3 //這種方式會報錯 invalid operation: m["three"] (type *map[string]int does not support indexing) (*m)["three"] = 3 //正確 fmt.Printf("changeMap func %p\n", m) //changeMap func 0x0 }

指針類型和值類型

請記住這句話:七個小矮人(slice,map,func,channel,pointer, string, interface),自帶魔法繩(指針), 所以沒必要將它定義成引用類型

 

 

這幅圖中展示了常用的值類型和引用類型(引用類型和傳引用是兩個概念)。在左邊是我們常用的一些值類型,函數調用時需要使用指針修改底層數據;而右邊是“引用類型”,我們可以理解為它們的底層都是指針類型,所以右邊的類型在使用的時候會有些不同,下文中會舉例說明。

type Foo struct { Name string } var bar = "hello biezhi" // -------------方法返回值---------------- func returnValue() string { return bar } func returnPoint() *string { return &bar } // --------------方法入參----------------- func modifyNameByPoint(foo *Foo) { foo.Name = "emmmm " + foo.Name } func nameToUpper(foo Foo) string { foo.Name = strings.ToUpper(foo.Name) return foo.Name } // --------------實例方法----------------- func (foo Foo) setName(name string) { foo.Name = name } func (foo *Foo) setNameByPoint(name string) { foo.Name = name }

這里我列出了 3 組方法,分別是指針類型和值類型的示例。

 

總結

  • Go 不能進行指針運算 (明顯降低復雜度和出錯風險)

  • 指針傳遞是很廉價的,只占用 4 個或 8 個字節。當程序在工作中需要占用大量的內存,或很多變量,或者兩者都有,使用指針會減少內存占用和提高效率。

  • 指針也是一種類型,不同於一般類型,指針的值是地址,這個地址指向其他的內存,通過指針可以讀取其所指向的地址所存儲的值。

  • 函數方法的接受者,也可以是指針變量。

  • 只聲明未賦值的變量,默認會初始化為零值,int類是0,float類是0,bool是false, string是空串,復數類型如complex64、complex128,默認值為0+0i,[4]int是{0,0,0,0},引用類型和指針的零值都為nil,比如error默認是nil, nil類型還沒有指向內存空間,不能直接賦值,因此需要通過new開辟一個內存地址,或指向一個已存在的變量地址。

  • [N]int是數組,而[]int才是slice,即固定長度是數組,數組被分配在stack里,slice如果>32 kB就分配在heap里,同時stack速度比heap快,heap空間比stack大。
  • 七個小矮人(slice,map,func,channel,pointer, string, interface),自帶魔法繩(指針), 所以沒必要將它定義成引用類型
  • struct、string實例,內存占用大則使用引用,小則無需引用
  • 純量(int類, float類, bool)無需使用引用
  • 變量的生命周期越長則使用指針,否則使用值類型,防止stack區滿了
  • 不想被修改,或者需要並發安全,可以使用值拷貝
  • 其他情況,優先用引用

參考資料

http//golang.org/doc/faq#Pointers

https://www.callicoder.com/golang-pointers/

https://www.ardanlabs.com/blog/2013/07/understanding-pointers-and-memory.html

https://www.ardanlabs.com/blog/2013/07/understanding-type-in-go.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM