go 指針和內存分配詳解

定義

瞭解指針以前,先講一下什麼是變量。html

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

如今,要訪問數據,咱們須要知道存儲它的地址。咱們能夠跟蹤存儲與程序相關的數據的全部內存地址。但想象一下,記住全部內存地址並使用它們訪問數據會有很是困難。這就是爲何引入變量。github

變量是一種佔位符,用於引用計算機的內存地址,可理解爲內存地址的標籤。golang

什麼是指針算法

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

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

Go類型佔用內存狀況

unsafe包能夠獲取變量的內存使用狀況函數

Go語言提供如下基本數字類型:性能

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

符號整數
int8,int16,int32,int64

實數
float32,float64 Predeclared

整數(依賴系統類型,跟系統有關)
uint,int,uintptr (指針)

32位系統

uint=uint32
int=int32
uintptr爲32位的指針

64位系統

uint=uint64
int=int64
uintptr爲64位的指針

示例:

package main

import (
   "fmt"
   "unsafe"
)

func main() {
   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

   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

}

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

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)

}

輸出:

example Size: 8
Alignment Boundary: 8
BoolValue = Size: 1 Offset: 0 Addr: 0xc00004c080
IntValue = Size: 2 Offset: 2 Addr: 0xc00004c082
FloatValue = Size: 4 Offset: 4 Addr: 0xc00004c084
Next = Size: 1 Offset: 0 Addr: 0xc00004c088

類型結構的對齊邊界是預期的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
}

總結:

  • Go 不能進行指針運算。
  • 指針傳遞是很廉價的,只佔用 4 個或 8 個字節。當程序在工做中須要佔用大量的內存,或不少變量,或者二者都有,使用指針會減小內存佔用和提升效率。
  • 指針也是一種類型,不一樣於通常類型,指針的值是地址,這個地址指向其餘的內存,經過指針能夠讀取其所指向的地址所存儲的值。
  • 函數方法的接受者,也能夠是指針變量。簡單類型和複雜類型在傳遞的時候不一樣,複雜類型傳值或傳指針都是指針拷貝。
  • 只聲明未賦值的變量,golang都會自動爲其初始化爲零值,基礎數據類型的零值比較簡單,引用類型和指針的零值都爲nil,nil類型不能直接賦值,所以須要經過new開闢一個內存,或指向一個變量。

參考資料

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

https://www.callicoder.com/go...

https://www.ardanlabs.com/blo...

https://www.ardanlabs.com/blo...

links

相關文章
相關標籤/搜索