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中,未定義的指針類型可以被寫成*T
,T
可以是任意類型。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)
}
下圖揭示了上面程序存儲的值間關系
為什么我們需要指針
讓我們先來看一個樣例程序
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中,指針不能進行算術運算。對于指針p
,p++
和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
,需要滿足如下兩個條件
-
T1
和T2
的底層類型相同(忽略結構體Tag)。特別地,如果T1
和T2
都是未定義類型并且它們的底層類型相同(考慮結構體Tag),可以進行隱式轉換。 -
T1
和T2
都是未定義指針類型,并且它們的基礎類型的底層類型相同(忽略結構體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
。