Golang通脈之指針


指針的概念

指針是存儲另一個變量的內存地址的變量。

變量是一種使用方便的占位符,用於引用計算機內存地址。

一個指針變量可以指向任何一個值的內存地址。

在上面的圖中,變量b的值為156,存儲在內存地址0x1040a124變量a持有b的地址,現在a被認為指向b

區別於C/C++中的指針,Go語言中的指針不能進行偏移和運算,是安全指針。

要搞明白Go語言中的指針需要先知道3個概念:指針地址、指針類型和指針取值。

Go語言中的指針不能進行偏移和運算,因此Go語言中的指針操作非常簡單,只需要記住兩個符號:&(取地址)和*(根據地址取值)。

聲明指針

聲明指針,*T是指針變量的類型,它指向T類型的值。

var var_name *var-type

var-type 為指針類型,var_name 為指針變量名,* 號用於指定變量是作為一個指針。

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮點型 */

示例代碼:

func main() {
   var a int= 20   /* 聲明實際變量 */
   var ip *int        /* 聲明指針變量 */

   ip = &a  /* 指針變量的存儲地址 */
   fmt.Printf("a 變量的地址是: %x\n", &a  )
   /* 指針變量的存儲地址 */
   fmt.Printf("ip 變量的存儲地址: %x\n", ip )
   /* 使用指針訪問值 */
   fmt.Printf("*ip 變量的值: %d\n", *ip )
}

運行結果:

a 變量的地址是: 20818a220
ip 變量的存儲地址: 20818a220
*ip 變量的值: 20

示例代碼:

type name int8
type first struct {
	a int
	b bool
	name
}

func main() {
	a := new(first)
	a.a = 1
	a.name = 11
	fmt.Println(a.b, a.a, a.name)
}

運行結果:

false 1 11

未初始化的變量自動賦上初始值

type name int8
type first struct {
	a int
	b bool
	name
}

func main() {
	var a = first{1, false, 2}
	var b *first = &a
	fmt.Println(a.b, a.a, a.name, &a, b.a, &b, (*b).a)
}

運行結果:

false 1 2 &{1 false 2} 1 0xc042068018 1

獲取指針地址在指針變量前加&的方式

指針地址和指針類型

每個變量在運行時都擁有一個地址,這個地址代表變量在內存中的位置。Go語言中使用&字符放在變量前面對變量進行“取地址”操作。 Go語言中的值類型(int、float、bool、string、array、struct)都有對應的指針類型,如:*int*int64*string等。

取變量指針的語法如下:

ptr := &v    // v的類型為T       取v的地址   其實就是把v的地址引用給了ptr,此時v和ptr指向了同一塊內存地址,任一變量值的修改都會影響另一個變量的值

其中:

  • v:代表被取地址的變量,類型為T
  • ptr:用於接收地址的變量,ptr的類型就為*T,稱做T的指針類型。*代表指針。
func main() {
   var a int = 10   
   fmt.Printf("變量a的地址: %x\n", &a  )
   b := &a
   fmt.Printf("變量b: %v\n", b  )
   fmt.Printf("變量b的地址: %v\n", &b  )
}

運行結果:

變量a的地址: 0x20818a220
變量b: 0x20818a220
變量b的地址: 0x263e94530

b := &a的圖示:

指針取值

在對普通變量使用&操作符取地址后會獲得這個變量的指針,然后可以對指針使用*操作,也就是指針取值,代碼如下。

func main() {
	//指針取值
	a := 10
	b := &a // 取變量a的地址,將指針保存到b中
	fmt.Printf("type of b:%T\n", b)
	c := *b // 指針取值(根據指針去內存取值)
	fmt.Printf("type of c:%T\n", c)
	fmt.Printf("value of c:%v\n", c)
}

輸出如下:

type of b:*int
type of c:int
value of c:10

總結: 取地址操作符&和取值操作符*是一對互補操作符,&取出地址,*根據地址取出地址指向的值。

變量、指針地址、指針變量、取地址、取值的相互關系和特性如下:

  • 對變量進行取地址(&)操作,可以獲得這個變量的指針變量。
  • 指針變量的值是指針地址。
  • 對指針變量進行取值(*)操作,可以獲得指針變量指向的原變量的值。

使用指針傳遞函數的參數

func change(val *int) {  
    *val = 55
}
func main() {  
    a := 58
    fmt.Println("value of a before function call is",a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}

運行結果

value of a before function call is 58  
value of a after function call is 55  

不要將一個指向數組的指針傳遞給函數。使用切片。

假設想對函數內的數組進行一些修改,並且對調用者可以看到函數內的數組所做的更改。一種方法是將一個指向數組的指針傳遞給函數。

func modify(arr *[3]int) {  
    (*arr)[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

運行結果

[90 90 91]

示例代碼:

func modify(arr *[3]int) {  
    arr[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

運行結果

[90 90 91]

雖然將指針傳遞給一個數組作為函數的參數並對其進行修改,但這並不是實現這一目標的慣用方法。切片是首選:

func modify(sls []int) {  
    sls[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

運行結果:

[90 90 91]

Go不支持指針算法。

func main() {
	b := [...]int{109, 110, 111} p := &b p++ 
}

nvalid operation: p++ (non-numeric type *[3]int)

指針數組

有一種情況,我們可能需要保存數組,這樣就需要使用到指針。

const MAX int = 3

func main() {
   a := []int{10,100,200}
   var i int
   var ptr [MAX]*int;

   for  i = 0; i < MAX; i++ {
      ptr[i] = &a[i] /* 整數地址賦值給指針數組 */
   }

   for  i = 0; i < MAX; i++ {
      fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
   } 
}
結果
a[0] = 10
a[1] = 100
a[2] = 200

指針的指針

如果一個指針變量存放的又是另一個指針變量的地址,則稱這個指針變量為指向指針的指針變量。

func main() {

   var a int
   var ptr *int
   var pptr **int

   a = 3000

   /* 指針 ptr 地址 */
   ptr = &a

   /* 指向指針 ptr 地址 */
   pptr = &ptr

   /* 獲取 pptr 的值 */
   fmt.Printf("變量 a = %d\n", a )
   fmt.Printf("指針變量 *ptr = %d\n", *ptr )
   fmt.Printf("指向指針的指針變量 **pptr = %d\n", **pptr)
}
結果
變量 a = 3000
指針變量 *ptr = 3000
指向指針的指針變量 **pptr = 3000

指針作為函數參數

package main

import "fmt"

func main() {
   /* 定義局部變量 */
   var a int = 100
   var b int= 200

   fmt.Printf("交換前 a 的值 : %d\n", a )
   fmt.Printf("交換前 b 的值 : %d\n", b )

   /* 調用函數用於交換值
   * &a 指向 a 變量的地址
   * &b 指向 b 變量的地址
   */
   swap(&a, &b);

   fmt.Printf("交換后 a 的值 : %d\n", a )
   fmt.Printf("交換后 b 的值 : %d\n", b )
}

func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址的值 */
   *x = *y      /* 將 y 賦值給 x */
   *y = temp    /* 將 temp 賦值給 y */
}
結果
交換前 a 的值 : 100
交換前 b 的值 : 200
交換后 a 的值 : 200
交換后 b 的值 : 100

空指針

Go 空指針 當一個指針被定義后沒有分配到任何變量時,它的值為 nil。 nil 指針也稱為空指針。 nil在概念上和其它語言的null、None、nil、NULL一樣,都指代零值或空值。 一個指針變量通常縮寫為 ptr。

空指針判斷:

if(ptr != nil)     /* ptr 不是空指針 */
if(ptr == nil)    /* ptr 是空指針 */
func main() {
   var sp *string
   *sp = "張三"
   fmt.Println(*sp)
}

運行這些代碼,會看到如下錯誤信息:

panic: runtime error: invalid memory address or nil pointer dereference

這是因為指針類型的變量如果沒有分配內存,就默認是零值 nil,它沒有指向的內存,所以無法使用,強行使用就會得到以上 nil 指針錯誤。

指針使用

  1. 指針可以修改指向數據的值;
  2. 在變量賦值,參數傳值的時候可以節省內存。

注意事項

  1. 不要對 map、slice、channel 這類引用類型使用指針;
  2. 如果需要修改方法接收者內部的數據或者狀態時,需要使用指針;
  3. 如果需要修改參數的值或者內部數據時,也需要使用指針類型的參數;
  4. 如果是比較大的結構體,每次參數傳遞或者調用方法都要內存拷貝,內存占用多,這時候可以考慮使用指針;
  5. 像 int、bool 這樣的小數據類型沒必要使用指針;
  6. 如果需要並發安全,則盡可能地不要使用指針,使用指針一定要保證並發安全;
  7. 指針最好不要嵌套,也就是不要使用一個指向指針的指針,雖然 Go 語言允許這么做,但是這會使代碼變得異常復雜。

new 和 make

我們知道對於值類型來說,即使只聲明一個變量,沒有對其初始化,該變量也會有分配好的內存。

func main() {
   var s string
   fmt.Printf("%p\n",&s)
}

結構體也是值類型,比如 var wg sync.WaitGroup 聲明的變量 wg ,不進行初始化也可以直接使用,Go 語言自動分配了內存,所以可以直接使用,不會報 nil 異常。

於是可以得到結論:如果要對一個變量賦值,這個變量必須有對應的分配好的內存,這樣才可以對這塊內存操作,完成賦值的目的。

其實不止賦值操作,對於指針變量,如果沒有分配內存,取值操作一樣會報 nil 異常,因為沒有可以操作的內存

所以一個變量必須要經過聲明、內存分配才能賦值,才可以在聲明的時候進行初始化。指針類型在聲明的時候,Go 語言並沒有自動分配內存,所以不能對其進行賦值操作,這和值類型不一樣。map 和 chan 也一樣,因為它們本質上也是指針類型。

要分配內存,就引出來了內置函數new()和make()。 Go語言中new和make是內建的兩個函數,主要用來分配內存。

new

new是一個內置的函數,它的函數簽名如下:

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

其中,

  • Type表示類型,new函數只接受一個參數,這個參數是一個類型
  • *Type表示類型指針,new函數返回一個指向該類型內存地址的指針。

它的作用就是根據傳入的類型申請一塊內存,然后返回指向這塊內存的指針,指針指向的數據就是該類型的零值:

func main() {
	a := new(int)
	b := new(bool)
	fmt.Printf("%T\n", a) // *int
	fmt.Printf("%T\n", b) // *bool
	fmt.Println(*a)       // 0
	fmt.Println(*b)       // false
}	

通過 new 函數分配內存並返回指向該內存的指針后,就可以通過該指針對這塊內存進行賦值、取值等操作。

make

make也是用於內存分配的,區別於new,它只用於slicemap以及chan的內存創建,而且它返回的類型就是這三個類型本身,而不是他們的指針類型,因為這三種類型就是引用類型,所以就沒有必要返回他們的指針了。make函數的函數簽名如下:

func make(t Type, size ...IntegerType) Type

在使用 make 函數創建 map 的時候,其實調用的是 makemap 函數:

// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap{
  //省略無關代碼
}

makemap 函數返回的是 *hmap 類型,而 hmap 是一個結構體,它的定義如下所示:

// A header for a Go map.
type hmap struct {
   // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
   // Make sure this stays in sync with the compiler's definition.
   count     int // # live cells == size of map.  Must be first (used by len() builtin)
   flags     uint8
   B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
   noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
   hash0     uint32 // hash seed
   buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
   oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
   nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
   extra *mapextra // optional fields
}

可以看到, map 關鍵字其實非常復雜,它包含 map 的大小 count、存儲桶 buckets 等。要想使用這樣的 hmap,不是簡單地通過 new 函數返回一個 *hmap 就可以,還需要對其進行初始化,這就是 make 函數要做的事情:

m:=make(map[string]int,10)

其實 make 函數就是 map 類型的工廠函數,它可以根據傳遞它的 K-V 鍵值對類型,創建不同類型的 map,同時可以初始化 map 的大小。

make 函數不只是 map 類型的工廠函數,還是 chan、slice 的工廠函數。它同時可以用於 slice、chan 和 map 這三種類型的初始化。

make函數是無可替代的,在使用slice、map以及channel的時候,都需要使用make進行初始化,然后才可以對它們進行操作。

new與make的區別

  1. 二者都是用來做內存分配的。
  2. make只用於slice、map以及channel的初始化,返回的還是這三個引用類型本身;
  3. new用於類型的內存分配,並且內存對應的值為類型零值,返回的是指向對應類型零值的指針。


免責聲明!

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



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