Go語言基礎之指針


Go語言基礎之指針

區別於C/C++中的指針,Go語言中的指針不能進行偏移和運算,是安全指針。

要搞明白Go語言中的指針需要先知道3個概念:指針地址、指針類型和指針取值。

一、Go語言中的指針

Go語言中的函數傳參都是值拷貝,當我們想要修改某個變量的時候,我們可以創建一個指向該變量地址的指針變量。傳遞數據使用指針,而無須拷貝數據。類型指針不能進行偏移和運算。Go語言中的指針操作非常簡單,只需要記住兩個符號:&(取地址)和*(根據地址取值)。

二、指針地址和指針類型

每個變量在運行時都擁有一個地址,這個地址代表變量在內存中的位置。Go語言中使用&字符放在變量前面對變量進行“取地址”操作。 Go語言中的值類型(int、float、bool、string、array、struct)都有對應的指針類型,如:*int*int64*string等。

指針類型的空值是 nil -----> 引用類型的空值都是nil類型

取變量指針的語法如下:

ptr := &v    // v的類型為T

其中:

  • v:代表被取地址的變量,類型為T
  • ptr:用於接收地址的變量,ptr的類型就為*T,稱做T的指針類型。*代表指針。

舉個例子:

func main() {
    a := 10
    b := &a
    fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
    fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
    fmt.Println(&b)                    // 0xc00000e018
}

我們來看一下b := &a的圖示:

取变量地址图示

三、指針取值

在對普通變量使用&操作符取地址后會獲得這個變量的指針,然后可以對指針使用*操作,也就是指針取值,代碼如下。

func main() {
    //指針取值
    a := 10
    b := &a // 取變量a的地址,將指針保存到b中
    fmt.Printf("type of b:%T\n", b)
    c := *b // 指針取值(根據指針去內存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}

輸出如下:

type of b:*int
type of c:int
value of c:10

總結: 取地址操作符&取值操作符*是一對互補操作符,&取出地址,*根據地址取出地址指向的值。

變量、指針地址、指針變量、取地址、取值的相互關系和特性如下:

  • 對變量進行取地址(&)操作,可以獲得這個變量的指針變量。
  • 指針變量的值是指針地址
  • 對指針變量進行取值(*)操作,可以獲得指針變量指向的原變量的值。

指針傳值示例1:

func modify1(x int) {
    x = 100
}

func modify2(x *int) {
    *x = 100
}

func main() {
    a := 10
    modify1(a)
    fmt.Println(a) // 10
    modify2(&a)
    fmt.Println(a) // 100
}

指針傳值示例2:

// 不要向函數傳遞數組的指針(取數組的地址),而要傳遞切片
func main() {
	//通過函數傳遞,把原數組的值改變
	var a =[5]int{9,8,7,6}
	test6(&a) // 還有一個原因數組的大小不一致
	fmt.Println(a)
	//test7(&a)  //這個不行 還有一個原因數組的大小不一致
	test7(a[:]) // //這個不行 還有一個原因數組的大小不一致
	fmt.Println(a)
}

func test6(a *[5]int)  {
	(*a)[0]=999
	fmt.Println(a)  //理應該是個地址但是人家給你打印成了 &[999 8 7 6] 表示指向這個數組的指針
	fmt.Println(*a)
}
func test7(a []int) {
	a[0] = 999
	fmt.Println(a)
}

不要向函數傳遞數組的指針,而應該使用切片

假如我們想要在函數內修改一個數組,並希望調用函數的地方也能得到修改后的數組,一種解決方案是把一個指向數組的指針傳遞給這個函數。

package main

import (  
    "fmt"
)

func modify(arr *[3]int) {  
    (*arr)[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

在上面程序的第 13 行中,我們將數組的地址傳遞給了 modify 函數。在第 8 行,我們在 modify 函數里把 arr 解引用,並將 90 賦值給這個數組的第一個元素。程序會輸出 [90 90 91]

a[x] 是 (*a)[x] 的簡寫形式,因此上面代碼中的 (*arr)[0] 可以替換為 arr[0]。下面我們用簡寫形式重寫以上代碼。

package main

import (  
    "fmt"
)

func modify(arr *[3]int) {  
    arr[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

該程序也會輸出 [90 90 91]

這種方式向函數傳遞一個數組指針參數,並在函數內修改數組。盡管它是有效的,但卻不是 Go 語言慣用的實現方式。我們最好使用切片來處理。

接下來我們用[切片]來重寫之前的代碼。

package main

import (  
    "fmt"
)

func modify(sls []int) {  
    sls[0] = 90
}

func main() {  
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

在上面程序的第 13 行,我們將一個切片傳遞給了 modify 函數。在 modify 函數中,我們把切片的第一個元素修改為 90。程序也會輸出 [90 90 91]所以別再傳遞數組指針了,而是使用切片吧。上面的代碼更加簡潔,也更符合 Go 語言的習慣。

數組指針和指針數組

// 數組指針和指針數組
// 數組指針---> 指向數組的指針
//指針數組----> 數組中放指針

var a = [5]int{9, 8, 7, 6}
var b *[5]int = &a //b是指向數組的指針
fmt.Println(b)

x, y := 10, 11
var c [4]*int = [4]*int{&x, &y} //數組中放指針
fmt.Println(c)

四、new和make

我們先來看一個例子:

func main() {
    var a *int
    *a = 100
    fmt.Println(*a)

    var b map[string]int // 初始化為空
    b["沙河娜扎"] = 100
    fmt.Println(b) // 都會報錯
}

執行上面的代碼會引發panic,為什么呢? 在Go語言中對於引用類型的變量,我們在使用的時候不僅要聲明它還要為它分配內存空間,否則我們的值就沒辦法存儲而對於值類型的聲明不需要分配內存空間是因為它們在聲明的時候已經默認分配好了內存空間要分配內存,就引出來今天的new和make。 Go語言中new和make是內建的兩個函數,主要用來分配內存。

4.1 new

new是一個內置的函數,它的函數簽名如下:

func new(Type) *Type

其中,

  • Type表示類型,new函數只接受一個參數這個參數是一個類型
  • *Type表示類型指針,new函數返回一個指向該類型內存地址的指針。

new函數不太常用,使用new函數得到的是一個類型的指針,並且該指針對應的值為該類型的零值。舉個例子:

func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
}   

本節開始的示例代碼中var a *int只是聲明了一個指針變量a但是沒有初始化指針作為引用類型需要初始化后才會擁有內存空間,才可以給它賦值。應該按照如下方式使用內置的new函數對a進行初始化之后就可以正常對其賦值了:

func main() {
    var a *int
    a = new(int)
    *a = 10
    fmt.Println(*a)
}

<nil>
0xc0000a0098
10

4.2 make

make也是用於內存分配的,區別於new,它只用於slice、map以及chan的內存創建,而且它返回的類型就是這三個類型本身, ** 而不是他們的指針類型,因為這三種類型就是引用類型**,所以就沒有必要返回他們的指針了。make函數的函數簽名如下:

func make(t Type, size ...IntegerType) Type

make函數是無可替代的,我們在使用slice、map以及channel的時候,都需要使用make進行初始化,然后才可以對它們進行操作。這個我們在上一章中都有說明,關於channel我們會在后續的章節詳細說明。

本節開始的示例中var b map[string]int只是聲明變量b是一個map類型的變量,需要像下面的示例代碼一樣使用make函數進行初始化操作之后,才能對其進行鍵值對賦值:

func main() {
    var b map[string]int
    b = make(map[string]int, 10)
    b["沙河娜扎"] = 100
    fmt.Println(b)
}

4.3 new與make的區別

  1. 二者都是用來做內存分配的。
  2. make只用於slice、map以及channel的初始化,返回的還是這三個引用類型本身;
  3. 而new用於類型的內存分配,並且內存對應的值為類型零值,返回的是指向類型的指針。

五、總結

  1. 對變量進行取地址(&)操作,可以獲得這個變量的指針變量,指針變量的值是指針地址
  2. 對指針變量進行取值(*)操作,可以獲得指針變量指向的原變量的值 ptr := &v v 代表被取地址的變量,變量 v 的地址使用變量 ptr 進行接收,ptr 的類型為*T,稱做 T 的指針類型,*代表指針
  3. *數組指針指向數組的指針*[5]int和指針數組指數組中放指針[5]*int
  4. & 取地址符號,可以取變量的地址
  5. *放在類型之前(表示這個類型的指針) 放在變量前(表示解引用)
  6. 指針的空值是nil
  7. 在函數中修改原來的數組,把數組的地址傳到函數中,直接修改
  8. 在函數中修改原來的數組,兩種方式,傳指針,傳切片
  9. 指針不支持運算
  10. “棧逃逸”,形象一點說就是:因為害怕留在棧區內而被系統連同函數棧幀內所有變量一並帶走/團滅而逃了堆區上。堆區的內容被GC機制接管,直到變量不可訪達便會被GC回收,直接通過 go build -gcflags '-m -l' 就可以看到逃逸分析的過程和結果
  11. make只用於slice、map以及channel的初始化,返回的還是這三個引用類型本身;
  12. new用於類型的內存分配,並且內存對應的值為類型零值,返回的是指向類型的指針


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM