Golang研學:在用好Golang指標型別

ZetaChow曉程式碼發表於2019-04-26

Golang指標

在大部分面嚮物件語言如C++、C#、Java,在函式傳引數時除了基礎值型別,物件是通過引用方式傳遞的。

然而,在Go語言中,除了map、slice和chan,所有型別(包括struct)都是值傳遞的。

那麼,如何在函式外使用函式內處理後的變數呢?只能通過返回新變數嗎?

不,可以使用指標

大部分面嚮物件語言都很少有用到指標的場景了,但是在Go語言中有大量的指標應用場景,要成為一名合格的Gopher,必須瞭解。

概念

每一個變數都會分配一塊記憶體,資料儲存在記憶體中,記憶體有一個地址,就像門牌號,通過這個地址就可以找到裡面儲存的資料。

指標就是儲存這個記憶體地址的變數。

在Go語言中,用&取得變數的地址

//為了說明型別,我採用了顯性的變數定義方法,實際開發中更多的是用“:=”自動獲取型別變數型別
var mystr string = "Hello!"
var mystrP *string = &mystr

fmt.Println(mystrP)
複製程式碼

將以上程式碼敲入main函式中,go run,列印出的內容就是mystr的記憶體地址。mystrP就是mystr的指標變數。

*取得指標變數指向的記憶體地址的值

在之前的程式碼的後面增加一句程式碼:

fmt.Println(*mystrPointer)
複製程式碼

go run 執行後,可以看到列印出 mystr的值“Hello!”

符號*也用做定義指標型別的關鍵字。

例如:

var p *int
複製程式碼

指標應用場景

在其他OOP語言中,大多數情況是不需要花太多時間操作指標的,如Java、C#,物件的引用操作都已經交給了虛擬機器和框架。而Go經常會用到指標。原因主要有3個:

  1. Go語言中除了map、slice、chan外,其他型別在函式引數中都是值傳遞
  2. Go語言不是物件導向的語言,很多時候實現結構體方法時需要用指標型別實現引用結構體物件
  3. 指標也是一個型別,在實現介面interface時,結構體型別和其指標型別對介面的實現是不同的

接下來就分別介紹一下,期間會穿插一些簡單的程式碼片段,您可以建立一個Go檔案輸入程式碼,執行體驗一下。

函式中傳遞指標引數

Go語言都是值傳遞,例如:

package main

import "fmt"

func main() {
	i := 0
	f(i)
	fmt.Println(i)
}

func f(count int) {
	fmt.Println(count)
	count++
}
複製程式碼

結果:

0
0
複製程式碼

i在執行前後沒有變化

如果希望被函式呼叫後,i的值產生變化,f函式的引數就應該改為 *int 型別。如:

func main() {
	i := 0
	f(&i)
	fmt.Println(i)
}

func f(count *int) {
	fmt.Println(*count)
	(*count)++
}
複製程式碼
  1. f定義引數用 *int 替代 int,申明引數是一個int型別的指標型別
  2. 呼叫函式時,不能直接傳遞int的變數i,而要傳遞用&取得i的地址
  3. f函式內,引數count現在是指標了,不能直接列印,需要用*取出這個指標指向的地址裡儲存的值
  4. count的取值+1.
  5. 呼叫f函式,在主函式main裡列印i

可以看到結果

0
1
複製程式碼

i的值改變了。

Struct結構體指標型別方法

Go語言中給結構體定義方法

//定義一個結構體型別
type myStruct struct {
	Name string
}

//定義這個結構體的改名方法
func (m myStruct) ChangeName(newName string) {
	m.Name = newName
}

func main() {
	//建立這個結構體變數
	mystruct := myStruct{
		Name: "zeta",
	}

	//呼叫改名函式
	mystruct.ChangeName("Chow")

	//沒改成功
	fmt.Println(mystruct.Name)
}
複製程式碼

這樣的方法不會改掉結構體變數內的欄位值。 就算是結構體方法,如果不使用指標,方法內還是傳遞結構體的值。

現在我們改用指標型別定義結構體方法看看。

只修改 ChangeName 函式,用*myStruct型別替代myStruct

func (m *myStruct) ChangeName(newName string) {
	m.Name = newName
}
複製程式碼

再執行一次,可以看到列印出來的名字改變了。

當使用指標型別定義方法後,結構體型別的變數呼叫方法時會自動取得該結構體的指標型別並傳入方法。

指標型別的介面實現

最近在某問答平臺回答了一個Gopher的問題,大致內容是問為什麼不能用結構體指標型別實現介面方法?

看一下程式碼

//定義一個介面
type myInterface interface {
	ChangeName(string)
	SayMyName()
}

//定義一個結構體型別
type myStruct struct {
	Name string
}

//定義這個結構體的改名方法
func (m *myStruct) ChangeName(newName string) {
	m.Name = newName
}

func (m myStruct) SayMyName() {
	fmt.Println(m.Name)
}

//一個使用介面作為引數的函式
func SetName(s myInterface, name string) {
	s.ChangeName(name)
}

func main() {
	//建立這個結構體變數
	mystruct := myStruct{
		Name: "zeta",
	}

	SetName(mystruct, "Chow")

	mystruct.SayMyName()
}
複製程式碼

這段程式碼是無法編譯通過的,會提示

cannot use mystruct (type myStruct) as type myInterface in argument to SetName:
        myStruct does not implement myInterface (ChangeName method has pointer receiver)
複製程式碼

myStruct型別沒有實現介面方法ChangeName,也就是說func (m *myStruct) ChangeName(newName string) 並不算實現了介面,因為它是*myStruct型別實現的,而不是myStruct

改一改

在呼叫SetName時,用&mystruct 替代 mystruct:

SetName(&mystruct, "Chow")
複製程式碼

編譯執行,成功。

為什麼結構體型別實現的介面該結構體的指標型別也算實現了,而指標型別實現的介面,不算是該結構體實現了介面呢?

** 原因是,結構體型別定義的方法可以被該結構體的指標型別呼叫;而結構體型別呼叫該指標型別的方法時是被轉換成指標,不是直接呼叫。**

所以,&mystruct 直接實現了介面定義的ChangeNameSayMyName兩個方法,而mystruct只能實現了SayMyNamemystruct呼叫ChangeName方法其實轉換成指標型別後呼叫的,不算實現了介面。


到此Go語言指標型別的應用介紹差不多了。

總結一下:

  1. Go語言中指標非常常用,一定要掌握
  2. Go語言除了map、slice、chan其他都是值傳遞,引用傳遞一定要用指標型別
  3. 結構體型別定義方法要注意使用指標型別
  4. 介面實現方法時,用指標型別實現的介面函式只能算是指標型別實現的,用結構體型別實現的方法也作為是指標型別實現。

歡迎大家一起討論、學習Go語言!!

加入Go曉程式碼QQ群:887038173

相關文章