教女朋友寫方法 -- 就要學習 Go 語言

Seekload發表於2019-03-03

這是『就要學習 Go 語言』系列的第 17 篇分享文章

剛接觸 Go 語言的函式和方法時,我產生過這樣的疑惑:為什麼會嚴格區分這兩者的概念?學完之後才知道,不像別的語言(Java、PHP等)函式即方法,方法即函式,Go 語言中兩者還是有很大區別的。

方法定義

定義方法與函式類似,區別在於:方法定義時,在 func 和方法名之間會增加一個額外的引數。如下:

func (receiver Type) methodName(...Type) Type {
    ...
}
複製程式碼

(receiver Type) 是增加的額外引數,receiver 稱為接收者,Type 可以是任意合法的型別,包括:結構體型別或新定義的型別。可以說,方法 methodName 屬於型別 Type。方法名後面的引數和返回值是可選的。

type Employee struct {
	FirstName,LastName string
}

func (e Employee) fullName() string {
	return e.FirstName + " " + e.LastName
}

func main() {
	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
	}
	fmt.Println(e.fullName())
}
複製程式碼

輸出

Jim Green
複製程式碼

採用 Type.methodName(...) 語法呼叫型別的方法。 上面的程式碼,定義了一個不用傳參、返回值為 string型別的方法 fullName。它屬於結構體型別 Employee,我們可以使用 e.fullName() 呼叫。fullName 中的 e 就是接收者,在方法內部可以訪問結構體的每一個成員。

現在,我們應該很清楚,方法與函式的區別:方法屬於某一種型別,且有接收者

值接收者和指標接收者

到目前為止,建立的方法使用的都是值接收者,還可以通過下面的語法建立指標接收者的方法:

func (receiver *Type) methodName(...Type) Type {
    ...
}
複製程式碼

值接收者和指標接收者,最大區別在於:在方法中修改指標接收者的值會影響到呼叫者的值,而值接收者就不會。一個是值的副本,一個是指標的副本,而指標的副本指向的還是原來的值。

type Employee struct {
	FirstName,LastName string
	age int
}

func (e Employee)changeFirstName(name string)  {
	e.FirstName = name
	fmt.Println("changeFirstName",e)
}

func (e *Employee)changeAge(age int)  {
	e.age = age
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		age:30,
	}
	fmt.Println("changebefore",e)
	e.changeFirstName("firstName")
	fmt.Println("changeName",e)
	(&e).changeAge(18)
	fmt.Println("changeAge",e)
}
複製程式碼

輸出

changebefore {Jim Green 30}
changeFirstName {firstName Green 30}
changeName {Jim Green 30}
changeAge {Jim Green 18}
複製程式碼

上面的程式碼,方法 changeFirstName() 使用的是值接收者,在方法中修改結構體的成員 FirstName 沒有影響到原來的值;而方法 changeAge() 使用的是指標接收者,在方法中修改結構體成員 age,原來的值也被改變了。

不知道你有沒有注意到,上面的程式碼中,呼叫指標接收者的方法時使用的是指標:(&e).changeAge(18) 。其實,平時編寫程式碼的時候,可以寫成:e.changeAge(18),編譯器會自動幫我們轉成指標,以滿足接收者的要求。同理,e.changeFirstName("firstName") 也可以寫成 (&e).changeFirstName("firstName") ,但這樣寫就複雜,一般不這麼做。

我們應該考慮不同的場景使用值接收者還是指標接收者,如果在方法中發生的改變對呼叫者可見或者變數拷貝成本比較高的,就應該考慮使用指標接收者,其他情況建議使用值接收者。例如:大變數 A,佔用記憶體大,使用值接收者的話拷貝成本高且效率低,這時就應該考慮使用指標接收者。

巢狀結構體的方法

我們這裡講雙層巢狀的結構體,外層稱為父結構體,結構體成員稱為子結構體,例如:

type Contact struct {
	phone,adress string
}
type Employee struct {
	FirstName,LastName string
	contact Contact
}
複製程式碼

Employee 是一個巢狀的結構體型別,稱為父結構體,成員變數 contact 也是一個結構體,型別是 Contact,稱為子結構體。

父結構體的方法,非匿名的成員結構體
type Contact struct {
	phone,adress string
}

type Employee struct {
	FirstName,LastName string
	contact Contact
}

func (e *Employee)changePhone(newPhone string){
	e.contact.phone = newPhone       // 注意訪問方式
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		contact:Contact{
			phone:"111",
			adress:"HangZhou",
		},
	}
	fmt.Println("before:",e)
	e.changePhone("222")
	fmt.Println("after:",e)
}
複製程式碼

輸出

before: {Jim Green {111 HangZhou}}
after: {Jim Green {222 HangZhou}}
複製程式碼

上面的程式碼,e 是巢狀結構體,在方法 changePhone() 中修改 contact 的成員 phone,注意修改的程式碼。

父結構體的方法,匿名的成員結構體
type Contact struct {
	phone,adress string
}

type Employee struct {
	FirstName,LastName string
	Contact
}

func (e *Employee)changePhone(newPhone string){
	// e.Contact.phone = newPhone   // 方式一
	e.phone = newPhone				// 方式二
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		Contact:Contact{
			phone:"111",
			adress:"HangZhou",
		},
	}
	fmt.Println("before:",e)
	e.changePhone("222")
	fmt.Println("after:",e)
}
複製程式碼

輸出結果與上面的一樣。 上面的程式碼,Contact 是一個匿名成員結構體。在方法 changePhone() 中修改成員 phone,注意修改的兩種方式。

子結構體的方法且非匿名
type Contact struct {
	phone,adress string
}

type Employee struct {
	FirstName,LastName string
	contact Contact
}

func (c *Contact)changePhone(newPhone string){
	c.phone = newPhone
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		contact:Contact{
			phone:"111",
			adress:"HangZhou",
		},
	}
	fmt.Println("before:",e)
	e.contact.changePhone("222")   // 注意呼叫方式,採用 .
	fmt.Println("after:",e)
}
複製程式碼

輸出結果與上面的一樣。 上面的程式碼,我們基於結構體型別 Contact 建立了方法 changePhone(),在方法中修改成員 phone,注意呼叫方法的方式。

子結構體的方法且匿名
type Contact struct {
	phone,adress string
}

type Employee struct {
	FirstName,LastName string
	Contact
}

func (c *Contact)changePhone(newPhone string){
	c.phone = newPhone
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		Contact:Contact{
			phone:"111",
			adress:"HangZhou",
		},
	}
	fmt.Println(e)
	// e.Contact.changePhone("222")	// 方式一
	e.changePhone("222")         	// 方式二
	fmt.Println(e)
}
複製程式碼

輸出結果與上面的一樣。 上面的程式碼,成員結構體 Contact 是匿名的,在方法 changePhone() 中修改成員 phone,注意呼叫方法的方式。

上面四個例子,希望能夠幫助大家更好理解巢狀結構體的方法!好,我們接著往下。

非結構體型別的方法

目前為止,都是在結構體上定義方法。文章開始提到了,可以在 Go 任一合法型別上定義方法,但是,有個問題:必須保證型別和方法定義在同一個包裡。之前,結構體和方法都定義在 main 包,所以可以執行。

package main
import "fmt"
func (i int)echo(){
	fmt.Println(i)
}

func main() {
	
}
複製程式碼

上面的程式碼,基於 int 型別建立了方法 echo(),由於 int 型別與方法 echo() 定義在不同的包內,所以編譯出錯:cannot define new methods on non-local type int。 那如何解決呢?你可能會想到,在 main 包內建立 int 型別別名,對!就是這樣:

package main
import "fmt"
type myInt int

func (i myInt) echo ()  {
	fmt.Println(i)
}

func main() {
	var a myInt
	a = 20
	a.echo()
}
複製程式碼

輸出:20 上面的程式碼,基於型別別名 myInt 建立了方法 echo,保證了型別和方法都 main 包。

為何需要方法

上面提到的例子,都是可以通過函式的方法實現的,回頭想想,Go 既然有了函式,為何需要方法呢?

  • Go 不是純粹的物件導向的語言且不支援類,通過型別的方法可以實現和類相似的功能,又不會像類那樣顯得很“重”;
  • 同名的方法可以定義在不同的型別上,但是函式名不允許相同。
type Rect struct {
	width  int
	height int
}

type Circle struct {
	radius float64
}

func (r Rect) Area() int {
	return r.width * r.height
}

func (c Circle) Area() float64 {
	return math.Pi * c.radius * c.radius
}

func main() {
	rect := Rect{5, 4}
	cir := Circle{5.0}
	fmt.Printf("Rect Area %d\n", rect.Area())
	fmt.Printf("Circle Area %0.2f\n", cir.Area())
}
複製程式碼

輸出

Rect Area 20
Circle Area 78.54
複製程式碼

上面的程式碼,在結構體 Rect 和 Circle 分別定義了同名的 Area() 方法,計算矩形和圓的面積。

學完這篇文章之後,相信你已經學會如何使用方法了,我們下一節再見!


(全文完)

原創文章,若需轉載請註明出處!
歡迎掃碼關注公眾號「Golang來啦」或者移步 seekload.net ,檢視更多精彩文章。

公眾號「Golang來啦」給你準備了一份神祕學習大禮包,後臺回覆【電子書】領取!

公眾號二維碼

相關文章