Go高階特性 12 | 指標詳解:在什麼情況下應該使用指標?

Swenson1992發表於2021-02-20

什麼是指標

我們都知道程式執行時的資料是存放在記憶體中的,而記憶體會被抽象為一系列具有連續編號的儲存空間,那麼每一個儲存在記憶體中的資料都會有一個編號,這個編號就是記憶體地址。有了這個記憶體地址就可以找到這個記憶體中儲存的資料,而記憶體地址可以被賦值給一個指標。

小提示:記憶體地址通常為 16 進位制的數字表示,比如 0x45b876。

可以總結為:在程式語言中,指標是一種資料型別,用來儲存一個記憶體地址,該地址指向儲存在該記憶體中的物件。這個物件可以是字串、整數、函式或者你自定義的結構體。

小技巧:你也可以簡單地把指標理解為記憶體地址。

舉個通俗的例子,每本書中都有目錄,目錄上會有相應章節的頁碼,可以把頁碼理解為一系列的記憶體地址,通過頁碼可以快速地定位到具體的章節(也就是說,通過記憶體地址可以快速地找到儲存的資料)。

指標的宣告和定義

在 Go 語言中,獲取一個變數的指標非常容易,使用取地址符 & 就可以,比如下面的例子:

func main() {
   name:="Hello Golang"
   nameP:=&name//取地址
   fmt.Println("name變數的值為:",name)
   fmt.Println("name變數的記憶體地址為:",nameP)
}

示例中定義了一個 string 型別的變數 name,它的值為”Hello Golang”,然後通過取地址符 & 獲取變數 name 的記憶體地址,並賦值給指標變數 nameP,該指標變數的型別為 *string。執行以上示例可以看到如下列印結果:

name變數的值為: Hello Golang
name變數的記憶體地址為: 0xc000010200

這一串 0xc000010200 就是記憶體地址,這個記憶體地址可以賦值給指標變數 nameP。

指標型別非常廉價,只佔用 4 個或者 8 個位元組的記憶體大小。

以上示例中 nameP 指標的型別是 *string,用於指向 string 型別的資料。在 Go 語言中使用型別名稱前加 * 的方式,即可表示一個對應的指標型別。比如 int 型別的指標型別是 *int,float64 型別的指標型別是 *float64,自定義結構體 A 的指標型別是 *A。總之,指標型別就是在對應的型別前加 * 號。

下面通過一個圖更好地理解普通型別變數、指標型別變數、記憶體地址、記憶體等之間的關係。

Go高階特性 12 | 指標詳解:在什麼情況下應該使用指標?

上圖就是剛舉的例子所對應的示意圖,從圖中可以看到普通變數 name 的值“Hello Golang”被放到記憶體地址為 0xc000010200 的記憶體塊中。指標型別變數也是變數,它也需要一塊記憶體用來儲存值,這塊記憶體對應的地址就是 0xc00000e028,儲存的值是 0xc000010200。相信你已經看到關鍵點了,指標變數 nameP 的值正好是普通變數 name 的記憶體地址,所以就建立指向關係。

小提示:指標變數的值就是它所指向資料的記憶體地址,普通變數的值就是我們具體存放的資料。

不同的指標型別是無法相互賦值的,比如你不能對一個 string 型別的變數取地址然後賦值給 *int指標型別,編譯器會提示你 Cannot use ‘&name’ (type *string) as type *int in assignment。

此外,除了可以通過簡短宣告的方式宣告一個指標型別的變數外,也可以使用 var 關鍵字宣告,如下面示例中的 var intP *int 就宣告瞭一個 *int 型別的變數 intP。

var intP *int
intP = &name //指標型別不同,無法賦值

可以看到指標變數也和普通的變數一樣,既可以通過 var 關鍵字定義,也可以通過簡短宣告定義。

小提示:通過 var 宣告的指標變數是不能直接賦值和取值的,因為這時候它僅僅是個變數,還沒有對應的記憶體地址,它的值是 nil。

和普通型別不一樣的是,指標型別還可以通過內建的 new 函式來宣告,如下所示

intP1:=new(int)

內建的 new 函式有一個引數,可以傳遞型別給它。它會返回對應的指標型別,比如上述示例中會返回一個 *int 型別的 intP1。

指標的操作

在 Go 語言中指標的操作無非是兩種:一種是獲取指標指向的值,一種是修改指標指向的值。

首先介紹如何獲取,用下面的程式碼進行演示:

nameV:=*nameP
fmt.Println("nameP指標指向的值為:",nameV)

可以看到,要獲取指標指向的值,只需要在指標變數前加 * 號即可,獲得的變數 nameV 的值就是“Hello Golang”,方法比較簡單。

修改指標指向的值也非常簡單,比如下面的例子:

*nameP = "Hello Golang" //修改指標指向的值
fmt.Println("nameP指標指向的值為:",*nameP)
fmt.Println("name變數的值為:",name)

對 *nameP 賦值等於修改了指標 nameP 指向的值。執行程式你將看到如下列印輸出:

nameP指標指向的值為: Hello Golang
name變數的值為: Hello Golang

通過列印結果可以看到,不光 nameP 指標指向的值被改變了,變數 name 的值也被改變了,這就是指標的作用。因為變數 name 儲存資料的記憶體就是指標 nameP 指向的記憶體,這塊記憶體被 nameP 修改後,變數 name 的值也被修改了。

通過 var 關鍵字直接定義的指標變數是不能進行賦值操作的,因為它的值為 nil,也就是還沒有指向的記憶體地址。比如下面的示例:

var intP *int
*intP =10

執行的時候會提示 invalid memory address or nil pointer dereference。這時候該怎麼辦呢?其實只需要通過 new 函式給它分配一塊記憶體就可以了,如下所示:

var intP *int = new(int)
//更推薦簡短宣告法,這裡是為了演示
//intP:=new(int)

指標引數

假如有一個函式 modifyAge,想要用來修改年齡,如下面的程式碼所示。但執行它,會看到 age 的值並沒有被修改,還是 18,並沒有變成 20。

age:=18
modifyAge(age)
fmt.Println("age的值為:",age)

func modifyAge(age int)  {
   age = 20
}

導致這種結果的原因是 modifyAge 中的 age 只是實參 age 的一份拷貝,所以修改它不會改變實參 age 的值。

如果要達到修改年齡的目的,就需要使用指標,如下所示:

age:=18
modifyAge(&age)
fmt.Println("age的值為:",age)
func modifyAge(age *int)  {
   *age = 20
}

也就是說,當需要在函式中通過形參改變實參的值時,需要使用指標型別的引數。

指標接收者

使用指標型別作為接收者,有以下幾點參考:

  • 如果接收者型別是 map、slice、channel 這類引用型別,不使用指標;
  • 如果需要修改接收者,那麼需要使用指標;
  • 如果接收者是比較大的型別,可以考慮使用指標,因為記憶體拷貝廉價,所以效率高。

所以對於是否使用指標型別作為接收者,還需要根據實際情況考慮。

什麼情況下使用指標

從以上指標的詳細分析中,可以總結出指標的兩大好處:

  1. 可以修改指向資料的值;
  2. 在變數賦值,引數傳值的時候可以節省記憶體。

使用指標的建議

  1. 不要對 map、slice、channel 這類引用型別使用指標;
  2. 如果需要修改方法接收者內部的資料或者狀態時,需要使用指標;
  3. 如果需要修改引數的值或者內部資料時,也需要使用指標型別的引數;
  4. 如果是比較大的結構體,每次引數傳遞或者呼叫方法都要記憶體拷貝,記憶體佔用多,這時候可以考慮使用指標;
  5. 像 int、bool 這樣的小資料型別沒必要使用指標;
  6. 如果需要併發安全,則儘可能地不要使用指標,使用指標一定要保證併發安全;
  7. 指標最好不要巢狀,也就是不要使用一個指向指標的指標,雖然 Go 語言允許這麼做,但是這會使程式碼變得異常複雜。
本作品採用《CC 協議》,轉載必須註明作者和本文連結
?

相關文章