認真一點學 Go:14. 指標型別

老苗發表於2021-10-14

>>原文地址

學到什麼

  1. 什麼是指標?

  2. 什麼是指標型別?

  3. 如何使用和建立指標型別變數?

  4. 如何從指標變數中取值?

  5. 如何傳遞指標?

什麼是指標

先了解什麼是記憶體地址?說通俗點就是電腦上資料儲存位置的編號,就好比我們的身份證號一樣。

指標也就是所說的記憶體地址,記憶體地址儲存在指標變數裡。

認真一點學 Go:14. 指標型別

圖解:圖中左半部分是一個字串資料,右半部分是指標變數,該指標變數儲存了字串資料的地址,圖中的地址純屬虛構。

指標型別

指標型別是在任意型別前增加星號,格式如下:


*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 協議》,轉載必須註明作者和本文連結

相關文章