跳至主要内容

[Golang] 指標 Pointers

Pointers @ A Tour of Go

TL;DR

  • 會需要「mutate」原本資料的 methods 就需要傳入的是 pointer
  • 單純是「顯示」原本資料用的 methods 就不需要傳入 pointer
// *T 是一種型別,指的是能夠指向該 T 的值的指標,它的 zero value 是 nil
// *T means pointer to value of type T
var p *int // nil

// &variable 會產生該 variable 的 pointer
i := 42
p := &i // & 稱作 address of pointer
fmt.Println(p) // 0xc0000b4008
fmt.Println(*p) // 透過 pointer 來讀取到 i
// 當 function receiver 這裡使用了 *type 時
// 這裡拿到的 p 會變成 pointer,指的是存放 p 的記憶體位址
func (p *person) updateNameFromPointer(newFirstName string) {
// *variable 表示把該指摽對應的值取出
p.firstName = newFirstName // 等同於 (*p).firstName = newFirstName
}

// 當沒有使用 *type 時
// 每次傳進來的 p 都會是複製一份新的(by value)
func (p person) updateName(newFirstName string) {
p.firstName = newFirstName
}

func main() {
jim := {
firstName: "Jim"
}

jim.updateNameFromPointer("Aaron") // It works as expected
jim.updateName("Aaron") // It doesn't work as expected
}

為什麼需要使用指標(Pointer)

指標(Pointer)是用來存放記憶體位置(Memory Address)

Go 是一個 pass by value 的程式語言,也就是每當我們把值放入函式中時,Go 會把這個值完整的複製一份,並放到新的記憶體位址,以下面的程式碼為例:

package main

import "fmt"

// 建立 person type
type person struct {
firstName string
lastName string
}

// 建立 person 的 function receiver
func (p person) updateName(newFirstName string) {
fmt.Printf("Before update: %+v\n", p)
p.firstName = newFirstName
fmt.Printf("After update: %+v\n", p)
}

func (p person) print() {
fmt.Printf("Current person is: %+v\n", p)
}

func main() {
jim := person{
firstName: "Jim",
lastName: "Party",
}

// Before update: {firstName:Jim lastName:Party}
jim.updateName("Aaron")
// After update: {firstName:Aaron lastName:Party}

jim.print() // Current person is: {firstName:Jim lastName:Party}
}

會發現到雖然呼叫了 jim.updateName() 這個方法,但 jim 的 firstName 並沒有改變。這是因為當我們呼叫 jim.updateName() 時,Go 會把呼叫此方法的 jim 複製一份到新的記憶體位置,修改的 jim 其實是存在另一個記憶體位置,這就是為什麼當我們在 updateName() 這個函式中呼叫 p 時,會看到 firstName 是有改變的,但最後呼叫 jim.print() 時,卻還是得到舊的 jim。

Imgur

什麼是指標(Pointer)

指標簡單來說,指的就是存放該變數的記憶體位址。

Imgur

備註:不只是 function receiver,function 中帶入的參數也是

⚠️ 在 Go 中雖然都是 pass by value,但要不要使用 Pointer 端看該變數的型別,某些型別會表現得類似 pass by reference,例如 slice,這時候就可以不用使用 Pointer 即可修改原變數的值。

  • 為什麼有些型別會表現的類似 pass by reference 可以參考「Array and Slice」的內容。
  • 哪些型別會表現的類似 pass by reference 則可以參考「資料型別(data type)」的內容。

以字串為例:

func main() {
jim := "Jim"
fmt.Println(jim) // Jim
changeName(jim)
fmt.Println(jim) // Jim
}

func changeName(person string) {
person = "Bob"
}

或者傳入的參數是 struct 也是一樣的:

type person struct{
firstName string
lastName string
}

// 直接傳入參數時,thePerson 會複製一份新的
func updateFirstName(thePerson person) {
thePerson.firstName = "Aaron"
}

// 透過 *type 傳入 pointer,會參照到原本的 thePerson
func updateFirstNameWithPointer(thePerson *person) {
(*thePerson).firstName = "Aaron"
}

func main() {
jim := person{
firstName: "Jim",
lastName: "Anderson",
}

fmt.Println(jim) // {Jim Anderson}
updateFirstName(jim)
fmt.Println(jim) // {Jim Anderson}

jimPointer := &jim
updateFirstNameWithPointer(jimPointer)
fmt.Println(jim) // {Aaron Anderson}
}

指標運算子(Pointers Operation)

& (ampersand) 和 *(Asterisk) 的使用

func main() {
// var p *int // nil

i, j := 42, 2701

p := &i // point to i
fmt.Println(p) // 0xc0000b4008
fmt.Println(*p) // 透過 pointer 來讀取到 i
*p = 21 // 透過 pointer 來設定 i 的值
fmt.Println(i) // 21

p = &j // 將 p 的值設為 j 的 pointer
*p = *p / 37 // 透過 pointer 來除 j
fmt.Println(j) // 73
}

Imgur

  • 當我們使用 &variable 時,會回傳該變數 value 的 address,表示給我這個變數值的「記憶體位置」
  • 當我們使用 *pointer 時,會回傳該 address 的 value,表示給我這個記憶體位置指稱到的「值」
  • ⚠️ 但若 * 是放在 type 前面,那個這麼 *不是運算子,而是對於該 type 的描述(type description)。因此在 func 中使用的 *type 是指要做事的對象是指稱到該型別(type)的指標(pointer),也就是這個 function 只能被 person 的指標(pointer to a person)給呼叫

Imgur

修改後的程式碼如下:

type person struct {
firstName string
lastName string
}

// 當 * 放在 type 前面時,這個 * 並不是運算子
// *type 指關於這個 type 的描述,也就是 pointer to the type
// *person 表示要針對「指稱到 person 的指標」做事
func (p *person) updateName(newFirstName string) {
// 使用 *pointer 則表示給我這個記憶體位址的「值」
// 所以 *p 會是 jimPointer 記憶體位址對應的值
(*p).firstName = newFirstName
}

func (p person) print() {
fmt.Printf("Current person is: %+v\n", p)
}

func main() {
jim := person{
firstName: "Jim",
lastName: "Party",
}

// 使用 &variable 可以得到該變數值的記憶體位置
jimPointer := &jim // jimPointer 是一個記憶體位址

jimPointer.updateName("Aaron")
jim.print() Current person is: {firstName:Aaron lastName:Party}
}

縮寫的使用

當我們在 function receiver 中使用 *type 後,這個函式將會自動把帶入的參數變成 pointer of the type

Imgur

因此,原本的程式碼可以不用先把它變成 pointer(可省略 jimPointer := &jim),直接縮寫成:

// 因為這裡有指稱要帶入的是 *person
func (p *person) updateName(newFirstName string) {
(*p).firstName = newFirstName
}

func main() {
jim := person{
firstName: "Jim",
lastName: "Party",
}

// 原本是這樣
// jimPointer := &jim
// jimPointer.updateName("Aaron")

// 所以,可以縮寫成,該 function 會自動去取 jim 的指標(記憶體位址)
jim.updateName("Aaron")
jim.print() // Current person is: {firstName:Aaron lastName:Party}
}

指標是 Reference Types 的變數

實際上指標(Pointer)本身和 Slice 一樣,都是屬於 Reference Types 的變數。從下面的例子中可以看到:

  • 使用 &name 取出 name 的 pointer 後,不論是 mainprintPointer 裡面的 namePointer 都指稱到同一個記憶體位置
  • 但由於 Go 本質上仍然是 Pass by Value,因此在 main 中的 &namePointer 會和 printPointer&namePointer 指向到不同的記憶體位址
  • 也就是說,當我把 Pointer 丟到函式的參數中時,實際上這個 Pointer 也被複製了一份新的(原本的 Pointer 的位址是 0xc0000ae018,複製到函式後是 0xc0000ae028,但這兩個記憶體位址,實際上都對應回 0xc00008e1e0
func main() {
name := "bill"
namePointer := &name

fmt.Println("1", namePointer) // 0xc00008e1e0
fmt.Println("2", &namePointer) // 0xc0000ae018
printPointer(namePointer)
}

func printPointer(namePointer *string) {
fmt.Println("3", namePointer) // 0xc00008e1e0
fmt.Println("4", &namePointer) // 0xc0000ae028
}

new(T)

new 是 golang 中內建的函式,使用 new(T) 分配這個 Type 所需的記憶體,並回傳一個可以指稱到它的 pointer,概念上和 : = T{} 差不多:

type Vertex struct {
X, Y float64
}
func main() {
v := new(Vertex) // new 回傳的是指稱到該變數的 pointer
v.X = 10
fmt.Println(&v) // {10, 0}
}

參考