學到什麼
什麼是指標?
什麼是指標型別?
如何使用和建立指標型別變數?
如何從指標變數中取值?
如何傳遞指標?
什麼是指標
先了解什麼是記憶體地址?說通俗點就是電腦上資料儲存位置的編號,就好比我們的身份證號一樣。
指標也就是所說的記憶體地址,記憶體地址儲存在指標變數裡。
圖解:圖中左半部分是一個字串資料,右半部分是指標變數,該指標變數儲存了字串資料的地址,圖中的地址純屬虛構。
指標型別
指標型別是在任意型別前增加星號,格式如下:
*BaseType
BaseType 代表任意型別。
例:
*int
表示int
型別變數的指標型別。*string
表示string
型別變數的指標型別。
type People struct {
Name string
Age int
}
*People
表示People
型別變數的指標型別。
如何建立指標變數
現在建立一個 *int
指標型別的變數,格式如下:
var p *int
p
是一個指標型別的變數,該變數還未初始化,現在給該變數初始化。
var num int = 11
p = &num
使用 &
符號獲取變數 num
的地址並賦值給指標變數 p
。
輸出指標變數資訊,如下:
fmt.Println(p)
// 輸出
0xc00000a088
0x
開頭說明是十六進位制,該十六進位制就是變數 num
的記憶體地址。
空指標
空指標表示指標變數沒有任何賦值,此時空指標變數等於 nil
。
var empty *int
fmt.Println(empty == nil)
// 輸出
true
nil
類似其它語言中的null
,在 Go 語言中只能和指標型別、介面型別進行比較,也只能給指標型別變數和介面型別變數賦值。
指標取值
宣告瞭一個指標變數後,如果想從指標變數中取值那該如何做,指標的取值常常被稱作解引用,格式如下:
var num int = 11
var p *int
p = &num
// 取值
fmt.Println(*p)
// 輸出
11
*p
表示從指標指向的變數num
中取出值,取值時在指標變數前增加一個*
符號。
如果指標變數是空指標,再從中取值時,編譯器會報錯。
panic: runtime error: invalid memory address or nil pointer dereference
結構體
如果指標變數是結構體指標型別時,獲取結構體中的欄位或呼叫方法時,無需在指標變數前增加*
。
p := &People{
Name: "老苗",
Age: 18,
}
fmt.Pringln(p)
fmt.Println(p.Name)
或
fmt.Println((*p).Name)
// 輸出
&{老苗 18}
老苗
總結:
結構體指標輸出的不是地址
呼叫結構體的欄位或方法時無需新增
*
方法
在上篇文章中已經接觸到了指標接收者的概念,這塊簡單說明一下,詳細的請看看《自定義型別和結構體 - 方法》。
如果通過方法想修改結構體中的欄位時,可以將接收者設定為指標型別。
// type/struct.go
// ...
func (p *People) SetName(name string) {
p.Name = name
}
func main() {
people := People{
Name: "老苗",
}
people.SetName("瀟灑哥")
fmt.Println(people.Name)
}
// 輸出
瀟灑哥
SetName
方法的接收者 p
為指標型別,修改了 Name
欄位,people
變數的資料也隨之更改。
指標傳遞
在 Go 語言中大部分的型別都是值傳遞,也就是說通過函式傳值時,函式內的修改是不能影響外部的,如果想更改就使用指標型別。
// pointer/function.go
// ...
func UpdateNum(num *int) {
*num = 2
}
func main() {
n := 1
UpdateNum(&n)
fmt.Println(n)
}
// 輸出
2
UpdateNum
函式接受一個 *int
指標型別,形參 num
指標型別指向了實參變數 n
,因此對 num
的修改影響了變數 n
的值。
對於 Go 語言中的個別型別本身就是引用型別,不需要使用指標就可以在函式內部修改值而影響外部。
1. map 和 通道
這兩個是引用型別,在傳遞時無需使用指標,通道在後續文章舉例講解。
// pointer/map.go
// ...
func SetCountry(countries map[string]string) {
countries["china"] = "中國"
}
func main() {
c := make(map[string]string)
SetCountry(c)
fmt.Println(c)
}
// 輸出
map[china:中國]
該程式碼初始化了一個 map
型別,然後通過 SetCountry
函式修改其值。
2. 切片
在瞭解《內建集合 - 切片》這篇文章後應該明白切片的底層引用的是陣列,在切片傳遞時不會改變底層陣列的引用,但如果對切片進行追加操作後,陣列引用就會改變。
// pointer/slice.go
//...
func AppendAnimals(animals []string) {
animals = append(animals, "老虎", "大象")
}
func main() {
input := []string{"猴子"}
AppendAnimals(input)
fmt.Println(input)
}
// 輸出
[猴子]
AppendAnimals
函式給切片追加元素,但外部的變數 input
的值不受影響,因為 append
操作後底層陣列會進行拷貝並改變引用。
如果在函式內想影響 input
變數,就使用指標解決。
// pointer/slice.go
//...
func AppendAnimalsPointer(animals *[]string) {
*animals = append(*animals, "老虎", "大象")
}
func main() {
input := []string{"猴子"}
AppendAnimalsPointer(&input)
fmt.Println(input)
}
// 輸出
[猴子 老虎 大象]
在傳遞切片時如果只修改切片內容,即不追加元素,原切片資料將會受到影響,因為底層陣列的引用沒有改變。
// pointer/slice.go
//...
func UpdateAnimals(animals []string) {
for i := range animals {
animals[i] = "兔子"
}
}
func main() {
updateInput := []string{"老虎", "大象"}
UpdateAnimals(updateInput)
fmt.Println(updateInput)
}
// 輸出
[兔子 兔子]
UpdateAnimals
函式修改了切片內容,通過輸出可以看出 updateInput
變數資料已改變。
總結
指標可以節省複製的開銷,但同時要考慮解引用和垃圾回收帶來的影響,所以不要把使用指標作為效能優化的首選方案。
本作品採用《CC 協議》,轉載必須註明作者和本文連結