Go 指針

Go中的指針

翻譯自

https://go101.org/article/pointer.html

盡管Go吸收了很多其他語言的特性,但Go總體來說是一個C家族語言。其中一個證據就是Go也支持指針。Go指針和C指針在許多方面非常相似,但其中也有一些不同。本文將會列舉Go指針的概念和細節。

Memory Address 內存地址

內存地址指的是整個系統管理(通常由操作系統管理)的內存空間中的偏移量(byte的個數)。

通常,內存地址被存儲成一個無符號(整型)字。字長在32位機器上是4字節,在64位機器上是8字節。所以理論最大尋址范圍,在32位機器上是2^32,也就是4GB。在64位機器上是2^64,也就是16EB(1EB=1024PB,1PB=1024TB,1TB=1024GB)。

內存地址通常用Hex表達模式書寫,如0x1234CDEF

Value Address 值地址

值的地址代表這個值在內存段中的起始地址

什么是指針?

指針是Go的一種類型。指針用來存儲內存地址。跟C語言不同,為了安全的原因,Go指針上有一些限制。

Go指針類型和值

在Go中,未定義的指針類型可以被寫成*TT可以是任意類型。T*T的基礎類型。

我們也可以自定義指針類型,通常由于未定義指針類型擁有更好的可讀性,不推薦使用自定義指針類型。

如果一個定義的指針類型的底層類型(underlying type)是*T,那么該指針的基礎類型是T。這個基礎類型相同的指針也是同一種類型。樣例

// 未定義的指針類型,基礎類型是int
*int
// 未定義的指針類型,基礎類型是*int
**int 

// Ptr是一個定義指針類型,基礎類型是int
type Ptr *int 
// PP是一個定義指針類型,基礎類型是Ptr
type PP *Ptr

指針的零值是nil,不存儲任何地址。

基礎類型為T的指針僅能存儲T類型值的地址

如何獲得指針值、什么是可尋址值

有兩種方式可用于獲取非nil的指針值

  • go內置的new函數,可以分配任何類型的內存。new(T)在內存中分配一個T值的空間,然后返回T值的地址。分配的值是T類型的零值。返回的地址就是一個T類型的指針
  • 我們還可以直接獲取可尋址值的地址。對于一個可尋址類型T的值t,我們可以使用&t來獲取t的地址,`&操作符用來獲取值地址。

通常上來說,可尋址值意味著該值存放在內存中的某處。現在,我們只需要知道任何變量都是可尋址的,同時常數、函數調用和顯示轉換的結果是不可尋址的。變量聲明的時候,Go運行時將會為這個變量分配一片內存,這片內存的開始地址就是這個變量的地址。

Pointer Derefernece 指針解引用

對于一個基礎類型為T的指針類型p,你如何獲取指針中存儲的值(或者說,被指針指向的值)?只需要使用表達式*p*被稱為解引用操作符。指針的解引用是取地址的逆運算。*p的返回值是T類型,也就是p的基礎類型。

nil指針進行解引用會導致運行時異常。

這個程序展示了地址獲取和解引用的例子:

package main

import "fmt"

func main() {
    // p0指向int的零值
    p0 := new(int)
    // hex表達的地址
    fmt.Println(p0)
    // 0
    fmt.Println(*p0)

    // x是p0指向值的拷貝
    x := *p0
    // 都取得x的地址
    // x, *p1, *p2 的值相同
    p1, p2 := &x, &x
    // true
    fmt.Println(p1 == p2)
    // false
    fmt.Println(p0 == p1)
    // p3 和 p0 也存儲相同的地址
    p3 := &*p0
    fmt.Println(p0 == p3)
    *p0, *p1 = 123, 789
    // 789 789 123
    fmt.Println(*p2, x, *p3)

    // int, int
    fmt.Printf("%T, %T \n", *p0, x)
    // *int, *int
    fmt.Printf("%T, %T \n", p0, p1)
}

下圖揭示了上面程序存儲的值間關系

image-20210712093648121

為什么我們需要指針

讓我們先來看一個樣例程序

package main

import "fmt"

func double(x int) {
    x += x
}

func main() {
    var a = 3
    double(a)
    fmt.Println(a) // 3
}

上例中的double函數預期對輸入值進行雙倍處理。但是它失敗了。為什么?因為所有值的分配,包括函數參數的傳遞,都是值拷貝。double函數操作的x只是a變量的拷貝,而不是a變量。

修復上例的一種方式是讓double函數返回一個新值。但這并不是所有場景都適用。下例展示了另一種使用指針的方案

package main

import "fmt"

func double(x *int) {
    *x += *x
    // 這行只為了解釋用途
    x = nil 
}

func main() {
    var a = 3
    double(&a)
    fmt.Println(a) // 6
    p := &a
    double(p)
    fmt.Println(a, p == nil) // 12 false
}

我們可以發現,通過將參數改為指針類型,傳遞的指針參數&a和它的拷貝x都指向相同的值,所以在*x上進行的修改,在a上也體現了出來。同時,因為參數傳遞都是值拷貝,上面將x賦值為nil,在p上也不生效。

簡而言之,指針提供了操作值的間接方式。大部分語言沒有指針的概念。然而,指針的概念只是隱藏在了語言的其他概念中。

返回局部變量指針在Go是安全的

和C語言不通,Go支持垃圾回收,所以返回局部變量的指針在Go中是絕對安全的

func newInt() *int {
    a := 3
    return &a
}

Go指針的限制

為了安全原因,相比C語言來說,Go在指針上做了一些限制。通過這些限制,Go保持了指針帶來的收益,并且避免了危險的指針使用。

Go指針不支持算術操作

在Go中,指針不能進行算術運算。對于指針pp++p-2都是非法的。

如果指針p指向一個數值,編譯器會將*p++識別為一個合法的語句,并解析為(*p)++。換句話說,*p操作的優先級高于++--操作符。樣例

package main

import "fmt"

func main() {
    a := int64(5)
    p := &a

    // 下面的語句無法編譯
    /*
    p++
    p = (&a) + 8
    */

    *p++
    fmt.Println(*p, a)   // 6 6
    fmt.Println(p == &a) // true

    *&a++
    *&*&a++
    **&p++
    *&*p++
    fmt.Println(*p, a) // 10 10
}

指針值不能轉換為任意的指針類型

在Go中,T1的指針值可以被隱式或是顯示地轉換為T2,需要滿足如下兩個條件

  • T1T2的底層類型相同(忽略結構體Tag)。特別地,如果T1T2都是未定義類型并且它們的底層類型相同(考慮結構體Tag),可以進行隱式轉換。
  • T1T2都是未定義指針類型,并且它們的基礎類型的底層類型相同(忽略結構體Tag)

舉個例子,有如下類型

type MyInt int64
type Ta *int64
type Tb *MyInt

有如下事實

  • *int64類型的值可以被隱式轉換為Ta類型,反過來也是可以的。因為它們的底層類型都是*int64
  • *MyInt類型的值可以被隱式轉換為Tb類型,反過來也行。因為它們的底層類型都是*MyInt
  • *MyInt類型的值可以被限制轉換為*int64,反之亦然。因為它們的基礎類型的底層類型都是int64
  • 即使是顯示轉換,Ta類型的值也不能直接轉換為Tb。因為Ta和底層類型和Tb不同,并且都是定義指針類型。不過可以進行連續幾次轉換,將Ta類型的pa間接轉化為Tb類型。 先將pa轉化為*int64類型(因為基礎類型的底層類型都是int64),再將*int64類型轉換為*MyInt類型,再將*MyInt類型轉化為*Tb類型。Tb((*MyInt)((*int64)(pa)))

上面的值通過任何安全的手段,都不能轉化為類型*uint64

任意兩個指針的值不能比較

在Go中,指針可以通過==!=符號比較。如果滿足如下任意一個條件,那么兩個Go指針的值可以進行比較

  • 兩個Go指針類型一致
  • 指針值可以隱式轉換為另一類型
  • 兩個指針中的一個且僅一個用無類型 nil 標識符表示。
package main

func main() {
    type MyInt int64
    type Ta    *int64
    type Tb    *MyInt

    // 4個不同類型的指針零值
    var pa0 Ta
    var pa1 *int64
    var pb0 Tb
    var pb1 *MyInt

    // 下面這6行都可以正常編譯
    // 比較結果都為true
    // 指針可以隱式轉換
    _ = pa0 == pa1
    // 指針類型一致
    _ = pb0 == pb1
    _ = pa0 == nil
    _ = pa1 == nil
    _ = pb0 == nil
    _ = pb1 == nil

    // 這三行都不能正常編譯
    /*
    _ = pa0 == pb0
    _ = pa1 == pb1
    _ = pa0 == Tb(nil)
    */
}

指針值不能賦值給其他指針類型

指針值互相賦值的條件和比較的條件一樣

有手段打破Go對指針的限制

unsafe 標準包提供的機制(特別是 unsafe.Pointer 類型)可以用來打破 Go 中對指針的限制。 unsafe.Pointer 類型類似于 C 中的 void*。一般不推薦使用unsafe

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容