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

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

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

方法定義

定義方法與函數相似,區別在於:方法定義時,在 func 和方法名之間會增長一個額外的參數。以下:學習

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

(receiver Type) 是增長的額外參數,receiver 稱爲接收者,Type 能夠是任意合法的類型,包括:結構體類型或新定義的類型。能夠說,方法 methodName 屬於類型 Type。方法名後面的參數和返回值是可選的。spa

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())
}
複製代碼

輸出.net

Jim Green
複製代碼

採用 Type.methodName(...) 語法調用類型的方法。 上面的代碼,定義了一個不用傳參、返回值爲 string類型的方法 fullName。它屬於結構體類型 Employee,咱們能夠使用 e.fullName() 調用。fullName 中的 e 就是接收者,在方法內部能夠訪問結構體的每個成員。指針

如今,咱們應該很清楚,方法與函數的區別:方法屬於某一種類型,且有接收者code

值接收者和指針接收者

到目前爲止,建立的方法使用的都是值接收者,還能夠經過下面的語法建立指針接收者的方法:cdn

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來啦」給你準備了一份神祕學習大禮包,後臺回覆【電子書】領取!

公衆號二維碼
相關文章
相關標籤/搜索