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