[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)
Go (Golang) Tutorial #14 - Pointers @ The Net Ninja
指標(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)G
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。
什麼是指標(Pointer)
指標簡單來說,指的就是存放該變數的記憶體位址。
備註:不只是 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
}
- 當我們使用
&variable
時,會回傳該變數 value 的 address,表示給我這個變數值的「記憶體位置」。 - 當我們使用
*pointer
時,會回傳該 address 的 value,表示給我這個記憶體位置指稱到的「值」。 - ⚠️ 但若
*
是放在type
前面,那個這麼*
並不是運算子,而是對於該 type 的描述(type description)。因此在 func 中使用的*type
是指要做事的對象是指稱到該型別(type)的指標(pointer),也就是這個 function 只能被 person 的指標(pointer to a person)給呼叫。
修改後的程式碼如下:
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:
因此,原本的程式碼可以不用先把它變成 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 後,不論是main
或printPointer
裡面的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}
}