Golang - 指標(Pointer)
本篇文章會簡單介紹指標,並學習如何使用指標與記憶體空間,使程式執行上能更有效能。
Pointer
簡介
學習 pointer 的第一條規則 : 不要害怕 !!
pointer 是一種 variable,他的內容是儲存另一個 variable 的 address,Address 則是每一個 variable 儲存在一個或多個連續的記憶體位置。
不同類型的 variable 會佔用不同的記憶體空間,像是 bool 只要一個 byte 就能代表 true 或 false,(因為可以獨立尋找 address 的最小空間是 byte),而 int32 需要 4 個 byte 的儲存空間。
雖然不同類型的 variable 可以佔用不同的記憶體空間,但每個 pointer 無論指向任何類型的 variable,都會是相同的大小。
var x int32 = 10
var y bool = true
pointerX := &x
pointerY := &y
var pointerZ *string
- pointerX 會是 x 的 address
- pointerY 會是 y 的 address
- pointerZ 不會指向任何東西,Value 會是 Zero value。
而 pointer 的 Zero value 是 nil
而不是 0,與 C 語言的 null 不同, nil
不代表 0,故不能將 nil 轉換成 0。
運算符
基本上 golang 的 pointer 運算符與 C/C++ 的相似。
&
: address operator,用在變數前的話,會返回該變數的 address。
*
: indirection operator,用於指針變數,會返回該 pointer 指向的 value。也被稱為 dereferencing。
x := 5
pointerX := &x
y := 5 + *pointerX
fmt.Println(y) // 10
dereferencing 要確保 pointer 不是 nil
,否則會造成 panic。
var x *int
fmt.Println(*x) // panic
指標類型
其實就是有一種類型叫做指標類型啦。用來表示 pointer,基本上可以基於任何類型。
x := 10
var pointerX *int
pointerX = &x
另外透過 new
聲明一個指針變數時,他的初始值會是 0 而不是 nil
。
var x = new(int)
fmt.Println(*x) // prints 0
傳遞(call)
簡介
當原始值分配給另一個變數或傳遞給 function or method 時,對其他變數所做的任何更改都不會反映在原始值中。
// Java
int x = 10;
int y = x;
y = 20;
System.out.println(x); // prints 10
然而在透過類型所建立的 instance,分配給另一個 instance 或傳遞給 function or method 時,又會是不同的結果。
// python
class Foo:
def __init__(self, x):
self.x = x
def outer():
f = Foo(10)
inner1(f)
print(f.x)
inner2(f)
print(f.x)
g = None
inner2(g)
print(g is None)
def inner1(f):
f.x = 20
def inner2(f):
f = Foo(30)
outer()
// Output
20
20
True
在許多程式語言中(e.g. Java, Python),會有以下特性:
- 如果將 instance 傳遞給函式並更改 field 的值,則這次更改會反映在傳遞進去的 instance。
- 如果重新分配參數,則更改不會反應在傳遞進去的變數。
- 如果傳遞
nil/null/None
等參考值,會將傳入參數本身設定為新的數值,而不會影響原先原先的數值。
因為這些語言的 instance 是透過 pointer 實現的,當 instance 傳遞給 function 或是 method 時,被複製的數值是指向該 instance 的 pointer。在 inner1
是指向相同的 address,而在 inner2
則是建立一個新的 instance,會指向不同的 address,因此不會影響到原先傳入的 instance。
基本上在 golang 中,會有一樣的特性,但 golang 不同的是可以對原始類型與架構使用 pointer 或是 value。
傳遞 pointer
上篇文章提到,golang 是 call by value,但可以透過將 pointer 傳遞給函式的方式,使原始變數被函式進行修改。只不過有幾點是要注意的。
如果傳遞 nil
pointer,則不能將數值修改為非 nil
。如果已經將該 pointer 分配了一個數值,則只能 reassign 這個數值。因為 call by value 的關係,會複製一份 pointer 變數,並在函式內處理,而原先的 pointer 當然就不會被函式修改到。
func failedUpdate(g *int) {
x := 10
g = &x
}
func main() {
var f *int // f is nil
failedUpdate(f)
fmt.Println(f) // prints nil
}
如果希望 pointer 參數傳入後修改的值在退出函式時仍然存在,不要在函式內建立一個新的變數並透過修改 pointer 來修改,而是透過 pointer 指向要修改的數值並進行修改。
func failedUpdate(px *int) {
x2 := 20
px = &x2
}
func update(px *int) {
*px = 20
}
func main() {
x := 10
failedUpdate(&x)
fmt.Println(x) // prints 10
update(&x)
fmt.Println(x) // prints 20
}
小心使用
基本上在 golang 是不建議使用 pointer,會影響數據傳遞的理解,除了在某些情況下使用會好些,例如在使用函式時需要一個 interface、部分類型需要以指針傳遞、數據類型中存在需要修改的狀態,會建議返回值使用指針類型。
效能
不過,pointer 的好處是無論任何的類型,pointer 都會是一樣的大小,也就是說當我們傳遞大的數值給函式時,花費的時間也會較多,但傳遞 pointer 則會減少原先所需傳遞的時間。
Slice
上一篇我們提到,Map 與 Slice 是以 pointer 實現的,故直接傳遞 Map 與 Slice 給函式是可以直接修改數值的,這邊提一下 Slice ,在函式內雖然可以修改內部數值,但不能修改 slice 的容量。
Slice 基本上為三個單元所組成:
- 資料,以 pointer 指向某一段記憶體空間。
- 長度,使用了多少空間。
- 容量,這個 slice 總共有多大。
因為 call by value 的關係,傳遞至函式內的 slice 會被複製,因為資料是以 pointer 指向某一段記憶體空間,所以可以直接修改,但容量部分只會修改函式內被複製的這份 slice,因此原先的 slice 並不會被修改到容量。
另外,slice 也很適合作為 buffer 使用,後面的文章會再提起 slice。
Garbage Collector
Garbage 是指沒有 pointer 指向該筆數據,則代表該數據存在記憶體空間中但沒有做使用,該數據就是 Garbage,這些空間就會被拿來重新使用,避免記憶體使用量一直增加。
Golang 有 Garbage Collector,但不代表可以隨性的使用記憶體空間。因為 Garbage Collector 也是需要資源去處理這些 Garbage,會降低程式的效能。
另外, Golang 會將原始類型、結構等放至 stack 中,並順序排列,透過建立最少的 Garbage,達到低延遲的目標。
寫著寫著發現 Stack, Garbage Collector 是個坑,之後有空可能會另外寫一篇出來吧。
後面會附上一些連結,有興趣的話可以參考。