Go 經(jīng)典入門系列 17:方法
歡迎來到 Golang 系列教程[1] 的第 17 個教程。
什么是方法?
方法其實就是一個函數(shù),在 func 這個關(guān)鍵字和方法名中間加入了一個特殊的接收器類型。接收器可以是結(jié)構(gòu)體類型或者是非結(jié)構(gòu)體類型。接收器是可以在方法的內(nèi)部訪問的。
下面就是創(chuàng)建一個方法的語法。
func?(t?Type)?methodName(parameter?list)?{
}
上面的代碼片段創(chuàng)建了一個接收器類型為 Type 的方法 methodName。
方法示例
讓我們來編寫一個簡單的小程序,它會在結(jié)構(gòu)體類型上創(chuàng)建一個方法并調(diào)用它。
package?main
import?(
????"fmt"
)
type?Employee?struct?{
????name?????string
????salary???int
????currency?string
}
/*
??displaySalary()?方法將?Employee?做為接收器類型
*/
func?(e?Employee)?displaySalary()?{
????fmt.Printf("Salary?of?%s?is?%s%d",?e.name,?e.currency,?e.salary)
}
func?main()?{
????emp1?:=?Employee?{
????????name:?????"Sam?Adolf",
????????salary:???5000,
????????currency:?"$",
????}
????emp1.displaySalary()?//?調(diào)用?Employee?類型的?displaySalary()?方法
}
在線運行程序[2]
在上面程序的第 16 行,我們在 Employee 結(jié)構(gòu)體類型上創(chuàng)建了一個 displaySalary 方法。displaySalary()方法在方法的內(nèi)部訪問了接收器 e Employee。在第 17 行,我們使用接收器 e,并打印 employee 的 name、currency 和 salary 這 3 個字段。
在第 26 行,我們調(diào)用了方法 emp1.displaySalary()。
程序輸出:Salary of Sam Adolf is $5000。
為什么我們已經(jīng)有函數(shù)了還需要方法呢?
上面的程序已經(jīng)被重寫為只使用函數(shù),沒有方法。
package?main
import?(
????"fmt"
)
type?Employee?struct?{
????name?????string
????salary???int
????currency?string
}
/*
displaySalary()方法被轉(zhuǎn)化為一個函數(shù),把 Employee 當(dāng)做參數(shù)傳入。
*/
func?displaySalary(e?Employee)?{
????fmt.Printf("Salary?of?%s?is?%s%d",?e.name,?e.currency,?e.salary)
}
func?main()?{
????emp1?:=?Employee{
????????name:?????"Sam?Adolf",
????????salary:???5000,
????????currency:?"$",
????}
????displaySalary(emp1)
}
在線運行程序[3]
在上面的程序中,displaySalary 方法被轉(zhuǎn)化為一個函數(shù),Employee 結(jié)構(gòu)體被當(dāng)做參數(shù)傳遞給它。這個程序也產(chǎn)生完全相同的輸出:Salary of Sam Adolf is $5000。
既然我們可以使用函數(shù)寫出相同的程序,那么為什么我們需要方法?這有著幾個原因,讓我們一個個的看看。
Go 不是純粹的面向?qū)ο缶幊陶Z言[4],而且Go不支持類。因此,基于類型的方法是一種實現(xiàn)和類相似行為的途徑。
相同的名字的方法可以定義在不同的類型上,而相同名字的函數(shù)是不被允許的。假設(shè)我們有一個
Square和Circle結(jié)構(gòu)體。可以在Square和Circle上分別定義一個Area方法。見下面的程序。
package?main
import?(
????"fmt"
????"math"
)
type?Rectangle?struct?{
????length?int
????width??int
}
type?Circle?struct?{
????radius?float64
}
func?(r?Rectangle)?Area()?int?{
????return?r.length?*?r.width
}
func?(c?Circle)?Area()?float64?{
????return?math.Pi?*?c.radius?*?c.radius
}
func?main()?{
????r?:=?Rectangle{
????????length:?10,
????????width:??5,
????}
????fmt.Printf("Area?of?rectangle?%d\n",?r.Area())
????c?:=?Circle{
????????radius:?12,
????}
????fmt.Printf("Area?of?circle?%f",?c.Area())
}
在線運行程序[5]
該程序輸出:
Area?of?rectangle?50
Area?of?circle?452.389342
上面方法的屬性被使用在接口中。我們將在接下來的教程中討論這個問題。
指針接收器與值接收器
到目前為止,我們只看到了使用值接收器的方法。還可以創(chuàng)建使用指針接收器的方法。值接收器和指針接收器之間的區(qū)別在于,在指針接收器的方法內(nèi)部的改變對于調(diào)用者是可見的,然而值接收器的情況不是這樣的。讓我們用下面的程序來幫助理解這一點。
package?main
import?(
????"fmt"
)
type?Employee?struct?{
????name?string
????age??int
}
/*
使用值接收器的方法。
*/
func?(e?Employee)?changeName(newName?string)?{
????e.name?=?newName
}
/*
使用指針接收器的方法。
*/
func?(e?*Employee)?changeAge(newAge?int)?{
????e.age?=?newAge
}
func?main()?{
????e?:=?Employee{
????????name:?"Mark?Andrew",
????????age:??50,
????}
????fmt.Printf("Employee?name?before?change:?%s",?e.name)
????e.changeName("Michael?Andrew")
????fmt.Printf("\nEmployee?name?after?change:?%s",?e.name)
????fmt.Printf("\n\nEmployee?age?before?change:?%d",?e.age)
????(&e).changeAge(51)
????fmt.Printf("\nEmployee?age?after?change:?%d",?e.age)
}
在線運行程序[6]
在上面的程序中,changeName 方法有一個值接收器 (e Employee),而 changeAge 方法有一個指針接收器 (e *Employee)。在 changeName 方法中對 Employee 結(jié)構(gòu)體的字段 name 所做的改變對調(diào)用者是不可見的,因此程序在調(diào)用 e.changeName("Michael Andrew") 這個方法的前后打印出相同的名字。由于 changeAge 方法是使用指針 (e *Employee) 接收器的,所以在調(diào)用 (&e).changeAge(51) 方法對 age 字段做出的改變對調(diào)用者將是可見的。該程序輸出如下:
Employee?name?before?change:?Mark?Andrew
Employee?name?after?change:?Mark?Andrew
Employee?age?before?change:?50
Employee?age?after?change:?51
在上面程序的第 36 行,我們使用 (&e).changeAge(51) 來調(diào)用 changeAge 方法。由于 changeAge 方法有一個指針接收器,所以我們使用 (&e) 來調(diào)用這個方法。其實沒有這個必要,Go語言讓我們可以直接使用 e.changeAge(51)。e.changeAge(51) 會自動被Go語言解釋為 (&e).changeAge(51)。
下面的程序[7]重寫了,使用 e.changeAge(51) 來代替 (&e).changeAge(51),它輸出相同的結(jié)果。
package?main
import?(
????"fmt"
)
type?Employee?struct?{
????name?string
????age??int
}
/*
使用值接收器的方法。
*/
func?(e?Employee)?changeName(newName?string)?{
????e.name?=?newName
}
/*
使用指針接收器的方法。
*/
func?(e?*Employee)?changeAge(newAge?int)?{
????e.age?=?newAge
}
func?main()?{
????e?:=?Employee{
????????name:?"Mark?Andrew",
????????age:??50,
????}
????fmt.Printf("Employee?name?before?change:?%s",?e.name)
????e.changeName("Michael?Andrew")
????fmt.Printf("\nEmployee?name?after?change:?%s",?e.name)
????fmt.Printf("\n\nEmployee?age?before?change:?%d",?e.age)
????e.changeAge(51)
????fmt.Printf("\nEmployee?age?after?change:?%d",?e.age)
}
在線運行程序[8]
那么什么時候使用指針接收器,什么時候使用值接收器?
一般來說,指針接收器可以使用在:對方法內(nèi)部的接收器所做的改變應(yīng)該對調(diào)用者可見時。
指針接收器也可以被使用在如下場景:當(dāng)拷貝一個結(jié)構(gòu)體的代價過于昂貴時。考慮下一個結(jié)構(gòu)體有很多的字段。在方法內(nèi)使用這個結(jié)構(gòu)體做為值接收器需要拷貝整個結(jié)構(gòu)體,這是很昂貴的。在這種情況下使用指針接收器,結(jié)構(gòu)體不會被拷貝,只會傳遞一個指針到方法內(nèi)部使用。
在其他的所有情況,值接收器都可以被使用。
匿名字段的方法
屬于結(jié)構(gòu)體的匿名字段的方法可以被直接調(diào)用,就好像這些方法是屬于定義了匿名字段的結(jié)構(gòu)體一樣。
package?main
import?(
????"fmt"
)
type?address?struct?{
????city??string
????state?string
}
func?(a?address)?fullAddress()?{
????fmt.Printf("Full?address:?%s,?%s",?a.city,?a.state)
}
type?person?struct?{
????firstName?string
????lastName??string
????address
}
func?main()?{
????p?:=?person{
????????firstName:?"Elon",
????????lastName:??"Musk",
????????address:?address?{
????????????city:??"Los?Angeles",
????????????state:?"California",
????????},
????}
????p.fullAddress()?//訪問?address?結(jié)構(gòu)體的?fullAddress?方法
}
在線運行程序[9]
在上面程序的第 32 行,我們通過使用 p.fullAddress() 來訪問 address 結(jié)構(gòu)體的 fullAddress() 方法。明確的調(diào)用 p.address.fullAddress() 是沒有必要的。該程序輸出:
Full?address:?Los?Angeles,?California
在方法中使用值接收器 與 在函數(shù)中使用值參數(shù)
這個話題很多Go語言新手都弄不明白。我會盡量講清楚。
當(dāng)一個函數(shù)有一個值參數(shù),它只能接受一個值參數(shù)。
當(dāng)一個方法有一個值接收器,它可以接受值接收器和指針接收器。
讓我們通過一個例子來理解這一點。
package?main
import?(
????"fmt"
)
type?rectangle?struct?{
????length?int
????width??int
}
func?area(r?rectangle)?{
????fmt.Printf("Area?Function?result:?%d\n",?(r.length?*?r.width))
}
func?(r?rectangle)?area()?{
????fmt.Printf("Area?Method?result:?%d\n",?(r.length?*?r.width))
}
func?main()?{
????r?:=?rectangle{
????????length:?10,
????????width:??5,
????}
????area(r)
????r.area()
????p?:=?&r
????/*
???????compilation?error,?cannot?use?p?(type?*rectangle)?as?type?rectangle
???????in?argument?to?area
????*/
????//area(p)
????p.area()//通過指針調(diào)用值接收器
}
在線運行程序[10]
第 12 行的函數(shù) func area(r rectangle) 接受一個值參數(shù),方法 func (r rectangle) area() 接受一個值接收器。
在第 25 行,我們通過值參數(shù) area(r) 來調(diào)用 area 這個函數(shù),這是合法的。同樣,我們使用值接收器來調(diào)用 area 方法 r.area(),這也是合法的。
在第 28 行,我們創(chuàng)建了一個指向 r 的指針 p。如果我們試圖把這個指針傳遞到只能接受一個值參數(shù)的函數(shù) area,編譯器將會報錯。所以我把代碼的第 33 行注釋了。如果你把這行的代碼注釋去掉,編譯器將會拋出錯誤 compilation error, cannot use p (type *rectangle) as type rectangle in argument to area.。這將會按預(yù)期拋出錯誤。
現(xiàn)在到了棘手的部分了,在第35行的代碼 p.area() 使用指針接收器 p 調(diào)用了只接受一個值接收器的方法 area。這是完全有效的。原因是當(dāng) area 有一個值接收器時,為了方便Go語言把 p.area() 解釋為 (*p).area()。
該程序?qū)敵觯?/p>
Area?Function?result:?50
Area?Method?result:?50
Area?Method?result:?50
在方法中使用指針接收器 與 在函數(shù)中使用指針參數(shù)
和值參數(shù)相類似,函數(shù)使用指針參數(shù)只接受指針,而使用指針接收器的方法可以使用值接收器和指針接收器。
package?main
import?(
????"fmt"
)
type?rectangle?struct?{
????length?int
????width??int
}
func?perimeter(r?*rectangle)?{
????fmt.Println("perimeter?function?output:",?2*(r.length+r.width))
}
func?(r?*rectangle)?perimeter()?{
????fmt.Println("perimeter?method?output:",?2*(r.length+r.width))
}
func?main()?{
????r?:=?rectangle{
????????length:?10,
????????width:??5,
????}
????p?:=?&r?//pointer?to?r
????perimeter(p)
????p.perimeter()
????/*
????????cannot?use?r?(type?rectangle)?as?type?*rectangle?in?argument?to?perimeter
????*/
????//perimeter(r)
????r.perimeter()//使用值來調(diào)用指針接收器
}
在線運行程序[11]
在上面程序的第 12 行,定義了一個接受指針參數(shù)的函數(shù) perimeter。第 17 行定義了一個有一個指針接收器的方法。
在第 27 行,我們調(diào)用 perimeter 函數(shù)時傳入了一個指針參數(shù)。在第 28 行,我們通過指針接收器調(diào)用了 perimeter 方法。所有一切看起來都這么完美。
在被注釋掉的第 33 行,我們嘗試通過傳入值參數(shù) r 調(diào)用函數(shù) perimeter。這是不被允許的,因為函數(shù)的指針參數(shù)不接受值參數(shù)。如果你把這行的代碼注釋去掉并把程序運行起來,編譯器將會拋出錯誤 main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.。
在第 35 行,我們通過值接收器 r 來調(diào)用有指針接收器的方法 perimeter。這是被允許的,為了方便Go語言把代碼 r.perimeter() 解釋為 (&r).perimeter()。該程序輸出:
perimeter?function?output:?30
perimeter?method?output:?30
perimeter?method?output:?30
在非結(jié)構(gòu)體上的方法
到目前為止,我們只在結(jié)構(gòu)體類型上定義方法。也可以在非結(jié)構(gòu)體類型上定義方法,但是有一個問題。為了在一個類型上定義一個方法,方法的接收器類型定義和方法的定義應(yīng)該在同一個包中。到目前為止,我們定義的所有結(jié)構(gòu)體和結(jié)構(gòu)體上的方法都是在同一個 main 包中,因此它們是可以運行的。
package?main
func?(a?int)?add(b?int)?{
}
func?main()?{
}
在線運行程序[12]
在上面程序的第 3 行,我們嘗試把一個 add 方法添加到內(nèi)置的類型 int。這是不允許的,因為 add 方法的定義和 int 類型的定義不在同一個包中。該程序會拋出編譯錯誤 cannot define new methods on non-local type int。
讓該程序工作的方法是為內(nèi)置類型 int 創(chuàng)建一個類型別名,然后創(chuàng)建一個以該類型別名為接收器的方法。
package?main
import?"fmt"
type?myInt?int
func?(a?myInt)?add(b?myInt)?myInt?{
????return?a?+?b
}
func?main()?{
????num1?:=?myInt(5)
????num2?:=?myInt(10)
????sum?:=?num1.add(num2)
????fmt.Println("Sum?is",?sum)
}
在線運行程序[13]
在上面程序的第5行,我們?yōu)?int 創(chuàng)建了一個類型別名 myInt。在第7行,我們定義了一個以 myInt 為接收器的的方法 add。
該程序?qū)蛴〕?Sum is 15。
我已經(jīng)創(chuàng)建了一個程序,包含了我們迄今為止所討論的所有概念,詳見github[14]。
這就是Go中的方法。祝你有美好的一天。
下一教程 - 接口 - I[15]
via: https://golangbot.com/methods/
作者:Nick Coghlan[16]譯者:MDGSF[17]校對:rxcai[18]
本文由 GCTT[19] 原創(chuàng)編譯,Go 中文網(wǎng)[20] 榮譽推出
參考資料
Golang 系列教程: https://studygolang.com/subject/2
[2]在線運行程序: https://play.golang.org/p/rRsI_sWAOZ
[3]在線運行程序: https://play.golang.org/p/dFwObgCUU0
[4]Go 不是純粹的面向?qū)ο缶幊陶Z言: https://golang.org/doc/faq#Is_Go_an_object-oriented_language
[5]在線運行程序: https://play.golang.org/p/0hDM3E3LiP
[6]在線運行程序: https://play.golang.org/p/tTO100HmUX
[7]程序: https://play.golang.org/p/nnXBsR3Uc8
[8]在線運行程序: https://play.golang.org/p/nnXBsR3Uc8
[9]在線運行程序: https://play.golang.org/p/vURnImw4_9
[10]在線運行程序: https://play.golang.org/p/gLyHMd2iie
[11]在線運行程序: https://play.golang.org/p/Xy5wW9YZMJ
[12]在線運行程序: https://play.golang.org/p/ybXLf5o_lA
[13]在線運行程序: https://play.golang.org/p/sTe7i1qAng
[14]github: https://github.com/golangbot/methods
[15]接口 - I: https://studygolang.com/articles/12266
[16]Nick Coghlan: https://golangbot.com/about/
[17]MDGSF: https://github.com/MDGSF
[18]rxcai: https://github.com/rxcai
[19]GCTT: https://github.com/studygolang/GCTT
[20]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
