昨天介紹了算符 (operators) 及零值 (zero values) ,今天就先來了解值與指標 (pointers) 吧!在知道什麼是指標之前,可以先簡單了解一下 Go 語言中其中一個記憶體管理系統 堆疊(stack) 。
舉例來說:
一般我們要把一個值 (A) 當作參數傳給 function 處理時,Go 會先將這個值複製建成一個新的變數 (新的 A) ,如果今天 function 要對參數做更動,因為已經有先複製成 (新的 A) ,則不會影響原來的值。
優點:可以減少程式碼錯誤,每個參數都會有屬於自己獨一無二的記憶體位置。
缺點:因為每次都要複製 (新的 A),複製本身會佔用過多記憶體位置。
為了節省記憶體位置,指標 (pointers) 就誕生啦!
指標為了解決堆疊的問題,所以他並不會複製值,那指標究竟是什麼呢?我通常把他想像成如上圖 (有名字的手指頭),當你今天想在資料庫找一個值 (A) 時,手指會指出值 (A) 的位置,指標因為他不會複製值,所以不會佔用無謂的記憶體空間。
當為一個值建立指標後, Go 語言會把他放在一個叫做 堆積 (heap) 的記憶體空間,而若是這個堆積的空間有一段時間都沒有指標指向他時,就會被 Go 丟掉,也就是 Go 語言裡的 垃圾回收機制 (garbage collection) ,也可以說是 Go 語言的堆積記憶體空間內,自帶打掃阿姨,幫你定期清你沒用到的垃圾。
優點:讓 function 呼叫起來更清爽,且可以簡化程式碼。
缺點:因為指標有複雜的垃圾回收機制,所以有時可能比原本的堆疊更 耗費 CPU 。
補充:
當對一個值建立指標後,不能同時使用堆疊來管理該值,因為魚與熊掌不可兼得(開玩笑的~),其實是因為堆疊會考慮到變數的作用範圍 (scope) ,但指標並不用。
var <變數> *<型別> // 在型別前加上 * 就可以宣告一個指標變數
補充:
用此種方法宣告指標變數,初始值為 nil 。
new() 函式可以 為型別取得記憶體位置、填入型別的零值,且傳回記憶體的指標
<變數> := new(<型別>)
var <變數> = new(<型別>)
<變數 1> := &<變數 2>
以下練習用上述三種方式,來建立指標,並試著印出來看看!
範例 1:
package main
import (
"fmt"
)
func main(){
var name *string // 宣告 name 指標變數,初始值為 nil
age := new(int) // 宣告 age 指標變數,初始值為 0
height := 160
myHeight := &height // 用 & 取得既有變數 (height) 的指標
fmt.Printf("name: %#v\n", name) // 用格式化輸出,型別+記憶體位置
fmt.Printf("age: %#v\n", age)
fmt.Printf("myHeight: %#v\n", myHeight)
}
範例 1(執行結果):
name: (*string)(nil)
age: (*int)(0xc000016098)
myHeight: (*int)(0xc0000160a0)
上面範例 1 我們使用 fmt.Printf() ,但卻只能印出指標變數的型別+記憶體位置,究竟要怎麼印出值呢?很簡單,只要在指標變數前加上 * 符號 ,就可以順利解除指標的參照 (dereference) 順利拿到他所指向的值啦!
以下根據範例 1 ,稍做修改,來拿拿看指標所指向的值:
範例 2:
package main
import (
"fmt"
)
func main(){
var name *string // 宣告 name 指標變數,初始值為 nil
age := new(int)
height := 160
myHeight := &height
fmt.Printf("name: %#v\n",*name) // 在指標變數前加上 * ,印出其值
fmt.Printf("age: %#v\n",*age)
fmt.Printf("myHeight: %#v\n",*myHeight)
}
範例 2(執行結果):
panic: runtime error: invalid memory address or nil pointer dereference
咦!怎麼噴錯了?別擔心,仔細看看錯誤訊息,他是在說指標指到一個 nil 值,其實這是一個常見的錯誤,我們解除指標參照時,不小心解到一個 nil 值,解決方法就是在解除參照前先檢查是否為 nil (如下)。
範例 2-1:
package main
import (
"fmt"
)
func main(){
var name *string // 宣告 name 指標變數,初始值為 nil
age := new(int)
height := 160
myHeight := &height
if name != nil {
fmt.Printf("name: %#v\n",*name) // 因為初始值為 nil,所以 if 不成立,不會印出
}
if age != nil {
fmt.Printf("age: %#v\n",*age)
}
if myHeight != nil {
fmt.Printf("myHeight: %#v\n",*myHeight)
}
}
範例 2-1(執行結果):
age: 0
myHeight: 160
若是一般變數,在 function 內的變動,只有在 function 內有作用,不會影響外面的世界,但若是指標變數傳入 function 則會改變原始的變數。
範例 3:
package main
import (
"fmt"
)
func addValue(count int) {
count += 10
fmt.Println("addValue:",count)
}
func addPoint(count *int) {
*count += 20
fmt.Println("addPoint:",*count)
}
func main(){
var count int
addValue(count)
fmt.Println("addValue in main:",count)
addPoint(&count)
fmt.Println("addPoint in main:",count)
}
範例 3(執行結果):
addValue: 10 // 在 addValue 這個 function 裡, count 的值為 10
addValue in main: 0 // 一般變數,在 function 內的變動,只有在 function 內有作用,故在外面 count 的值為初始值 0
addPoint: 20 // 在 addPoint 這個 function 裡, count 的值為 20
addPoint in main: 20 // 指標變數傳入 function 則會改變原始的變數,故 count 的值變為 20
今天介紹了在 Go 語言中,堆疊 (stack)、值與指標 (pointers) ,相信大家在實際了解其中差異之後,應該會對於何時要用指標更有想法了吧!
那我們明天繼續來介紹常數 (constants) 、列舉 (enums) 與變數作用範圍 (scope),明天見~