Go支持一些面向?qū)ο缶幊烫匦?,方法是這些所支持的特性之一。 本篇文章將介紹在Go中和方法相關(guān)的各種概念。
在Go中,我們可以為類型T
和*T
顯式地聲明一個(gè)方法,其中類型T
必須滿足四個(gè)條件:
類型T
和*T
稱為它們各自的方法的屬主類型(receiver type)。 類型T
被稱作為類型T
和*T
聲明的所有方法的屬主基類型(receiver base type)。
注意:我們也可以為滿足上列條件的類型T
和*T
的別名聲明方法。 這樣做的效果和直接為類型T
和*T
聲明方法是一樣的。
如果我們?yōu)槟硞€(gè)類型聲明了一個(gè)方法,以后我們可以說(shuō)此類型擁有此方法。
從上面列出的條件,我們得知我們不能為下列類型(顯式地)聲明方法:
int
和string
。 因?yàn)檫@些類型聲明在內(nèi)置builtin
標(biāo)準(zhǔn)包中,而我們不能在標(biāo)準(zhǔn)包中聲明方法。*T
的指針類型之外的無(wú)名組合類型。一個(gè)方法聲明和一個(gè)函數(shù)聲明很相似,但是比函數(shù)聲明多了一個(gè)額外的參數(shù)聲明部分。 此額外的參數(shù)聲明部分只能含有一個(gè)類型為此方法的屬主類型的參數(shù),此參數(shù)稱為此方法聲明的屬主參數(shù)(receiver parameter)。 此屬主參數(shù)聲明必須包裹在一對(duì)小括號(hào)()
之中。 此屬主參數(shù)聲明部分必須處于func
關(guān)鍵字和方法名之間。
下面是一個(gè)方法聲明的例子:
// Age和int是兩個(gè)不同的類型。我們不能為int和*int
// 類型聲明方法,但是可以為Age和*Age類型聲明方法。
type Age int
func (age Age) LargerThan(a Age) bool {
return age > a
}
func (age *Age) Increase() {
*age++
}
// 為自定義的函數(shù)類型FilterFunc聲明方法。
type FilterFunc func(in int) bool
func (ff FilterFunc) Filte(in int) bool {
return ff(in)
}
// 為自定義的映射類型StringSet聲明方法。
type StringSet map[string]struct{}
func (ss StringSet) Has(key string) bool {
_, present := ss[key]
return present
}
func (ss StringSet) Add(key string) {
ss[key] = struct{}{}
}
func (ss StringSet) Remove(key string) {
delete(ss, key)
}
// 為自定義的結(jié)構(gòu)體類型Book和它的指針類型*Book聲明方法。
type Book struct {
pages int
}
func (b Book) Pages() int {
return b.pages
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
從上面的例子可以看出,我們可以為各種種類(kind)的類型聲明方法,而不僅僅是結(jié)構(gòu)體類型。
在很多其它面向?qū)ο蟮木幊陶Z(yǔ)言中,屬主參數(shù)名總是為隱式聲明的this
或者self
。這樣的名稱不推薦在Go編程中使用。
指針類型的屬主參數(shù)稱為指針類型屬主,非指針類型的屬主參數(shù)稱為值類型屬主。 在大多數(shù)情況下,我個(gè)人非常反對(duì)將指針和值這兩個(gè)術(shù)語(yǔ)用做對(duì)立面,但是在這里,我并不反對(duì)這么用,原因?qū)⒃谙旅嬲劶啊?
方法名可以是空標(biāo)識(shí)符_
。一個(gè)類型可以擁有若干名可以是空標(biāo)識(shí)符的方法,但是這些方法無(wú)法被調(diào)用。 只有導(dǎo)出的方法才可以在其它代碼包中調(diào)用。 方法調(diào)用將在后面的一節(jié)中介紹。
對(duì)每個(gè)方法聲明,編譯器將自動(dòng)隱式聲明一個(gè)相對(duì)應(yīng)的函數(shù)。 比如對(duì)于上一節(jié)的例子中為類型Book
和*Book
聲明的兩個(gè)方法,編譯器將自動(dòng)聲明下面的兩個(gè)函數(shù):
func Book.Pages(b Book) int {
return b.pages // 此函數(shù)體和Book類型的Pages方法體一樣
}
func (*Book).SetPages(b *Book, pages int) {
b.pages = pages // 此函數(shù)體和*Book類型的SetPages方法體一樣
}
在上面的兩個(gè)隱式函數(shù)聲明中,它們各自對(duì)應(yīng)的方法聲明的屬主參數(shù)聲明被插入到了普通參數(shù)聲明的第一位。 它們的函數(shù)體和各自對(duì)應(yīng)的顯式方法的方法體是一樣的。
兩個(gè)隱式函數(shù)名Book.Pages
和(*Book).SetPages
都是aType.MethodName
這種形式的。 我們不能顯式聲明名稱為這種形式的函數(shù),因?yàn)檫@種形式中的函數(shù)名不屬于合法標(biāo)識(shí)符。這樣的函數(shù)只能由編譯器隱式聲明。 但是我們可以在代碼中調(diào)用這些隱式聲明的函數(shù):
package main
import "fmt"
type Book struct {
pages int
}
func (b Book) Pages() int {
return b.pages
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
func main() {
var book Book
// 調(diào)用這兩個(gè)隱式聲明的函數(shù)。
(*Book).SetPages(&book, 123)
fmt.Println(Book.Pages(book)) // 123
}
事實(shí)上,在隱式聲明上述兩個(gè)函數(shù)的同時(shí),編譯器也將改寫這兩個(gè)函數(shù)對(duì)應(yīng)的顯式方法(至少,我們可以這樣認(rèn)為),讓這兩個(gè)方法在體內(nèi)直接調(diào)用這兩個(gè)隱式函數(shù):
func (b Book) Pages() int {
return Book.Pages(b)
}
func (b *Book) SetPages(pages int) {
(*Book).SetPages(b, pages)
}
對(duì)每一個(gè)為值類型屬主T
聲明的方法,一個(gè)相應(yīng)的同名方法將自動(dòng)隱式地為其對(duì)應(yīng)的指針類型屬主*T
而聲明。 以上面的為類型Book
聲明的Pages
方法為例,一個(gè)同名方法將自動(dòng)為類型*Book
而聲明:
// 注意:這不是合法的Go語(yǔ)法。這里這樣表示只是
// 為了解釋目的。它表明表達(dá)式(&aBook).Pages
// 將被估值為aBook.Pages(見隨后幾節(jié))。
func (b *Book) Pages = (*b).Pages
正因?yàn)槿绱耍也⒉慌懦馐褂弥殿愋蛯僦鬟@個(gè)術(shù)語(yǔ)做為指針類型屬主這個(gè)術(shù)語(yǔ)的對(duì)立面。 畢竟,當(dāng)我們?yōu)橐粋€(gè)非指針類型顯式聲明一個(gè)方法的時(shí)候,事實(shí)上兩個(gè)方法被聲明了。 一個(gè)方法是為非指針類型顯式聲明的,另一個(gè)是為指針類型隱式聲明的。
上一節(jié)已經(jīng)提到了,每一個(gè)方法對(duì)應(yīng)著一個(gè)編譯器隱式聲明的函數(shù)。 所以對(duì)于剛提到的隱式方法,編譯器也將隱式聲明一個(gè)相應(yīng)的函數(shù):
func (*Book).Pages(b *Book) int {
return Book.Pages(*b)
}
換句話說(shuō),對(duì)于每一個(gè)為值類型屬主顯式聲明的方法,同時(shí)將有一個(gè)隱式方法和兩個(gè)隱式函數(shù)被自動(dòng)聲明。
一個(gè)方法描述可以看作是一個(gè)不帶func
關(guān)鍵字的函數(shù)原型。 我們可以把每個(gè)方法聲明看作是由一個(gè)func
關(guān)鍵字、一個(gè)屬主參數(shù)聲明部分、一個(gè)方法描述和一個(gè)方法體組成。
比如,上面的例子中的Pages
和SetPages
的描述如下:
Pages() int
SetPages(pages int)
每個(gè)類型都有個(gè)方法集。一個(gè)非接口類型的方法集由所有為它聲明的(不管是顯式的還是隱式的,但不包含方法名為空標(biāo)識(shí)符的)方法的描述組成。 接口類型將在下一篇文章詳述。
比如,在上面的例子中,Book
類型的方法集為:
Pages() int
而*Book
類型的方法集為:
Pages() int
SetPages(pages int)
方法集中的方法描述的次序并不重要。
對(duì)于一個(gè)方法集,如果其中的每個(gè)方法描述都處于另一個(gè)方法集中,則我們說(shuō)前者方法集為后者(即另一個(gè))方法集的子集,后者為前者的超集。 如果兩個(gè)方法集互為子集(或超集),則這兩個(gè)方法集必等價(jià)。
給定一個(gè)類型T
,假設(shè)它既不是一個(gè)指針類型也不是一個(gè)接口類型,因?yàn)樯弦还?jié)中提到的原因,類型T
的方法集總是類型*T
的方法集的子集。 比如,在上面的例子中,Book
類型的方法集為*Book
類型的方法集的子集。
請(qǐng)注意:不同代碼包中的同名非導(dǎo)出方法將總被認(rèn)為是不同名的。
方法集在Go中的多態(tài)特性中扮演著重要的角色。多態(tài)將在下一篇文章中講解。
下列類型的方法集總為空:
方法事實(shí)上是特殊的函數(shù)。方法也常被稱為成員函數(shù)。 當(dāng)一個(gè)類型擁有一個(gè)方法,則此類型的每個(gè)值將擁有一個(gè)不可修改的函數(shù)類型的成員(類似于結(jié)構(gòu)體的字段)。 此成員的名稱為此方法名,它的類型和此方法的聲明中不包括屬主部分的函數(shù)聲明的類型一致。 一個(gè)值的成員函數(shù)也可以稱為此值的方法。
一個(gè)方法調(diào)用其實(shí)是調(diào)用了一個(gè)值的成員函數(shù)。假設(shè)一個(gè)值v
有一個(gè)名為m
的方法,則此方法可以用選擇器語(yǔ)法形式v.m
來(lái)表示。
下面這個(gè)例子展示了如何調(diào)用為Book
和*Book
類型聲明的方法:
package main
import "fmt"
type Book struct {
pages int
}
func (b Book) Pages() int {
return b.pages
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
func main() {
var book Book
fmt.Printf("%T \n", book.Pages) // func() int
fmt.Printf("%T \n", (&book).SetPages) // func(int)
// &book值有一個(gè)隱式方法Pages。
fmt.Printf("%T \n", (&book).Pages) // func() int
// 調(diào)用這三個(gè)方法。
(&book).SetPages(123)
book.SetPages(123) // 等價(jià)于上一行
fmt.Println(book.Pages()) // 123
fmt.Println((&book).Pages()) // 123
}
(和C語(yǔ)言不同,Go中沒有->
操作符用來(lái)通過(guò)指針屬主值來(lái)調(diào)用方法。(&book)->SetPages(123)
在Go中是非法的。)
等一下,上例中的(&book).SetPages(123)
一行為什么可以被簡(jiǎn)化為book.SetPages(123)
呢? 畢竟,類型Book
并不擁有一個(gè)SetPages
方法。 啊哈,這可以看作是Go中為了讓代碼看上去更簡(jiǎn)潔而特別設(shè)計(jì)的語(yǔ)法糖。此語(yǔ)法糖只對(duì)可尋址的值類型的屬主有效。 編譯器會(huì)隱式地將book.SetPages(123)
改寫為(&book).SetPages(123)
。
但另一方面,我們應(yīng)該總是認(rèn)為aBookExpression.SetPages
是一個(gè)合法的選擇器(從語(yǔ)法層面講),即使表達(dá)式aBookExpression
被估值為一個(gè)不可尋址的Book
值(在這種情況下,aBookExpression.SetPages
是一個(gè)無(wú)效但合法的選擇器)。
如上面剛提到的,當(dāng)為一個(gè)類型聲明了一個(gè)方法后,每個(gè)此類型的值將擁有一個(gè)和此方法同名的成員函數(shù)。 此類型的零值也不例外,不論此類型的零值是否用nil
來(lái)表示。
一個(gè)例子:
package main
type StringSet map[string]struct{}
func (ss StringSet) Has(key string) bool {
_, present := ss[key] // 永不會(huì)產(chǎn)生恐慌,即使ss為nil。
return present
}
type Age int
func (age *Age) IsNil() bool {
return age == nil
}
func (age *Age) Increase() {
*age++ // 如果age是一個(gè)空指針,則此行將產(chǎn)生一個(gè)恐慌。
}
func main() {
_ = (StringSet(nil)).Has // 不會(huì)產(chǎn)生恐慌
_ = ((*Age)(nil)).IsNil // 不會(huì)產(chǎn)生恐慌
_ = ((*Age)(nil)).Increase // 不會(huì)產(chǎn)生恐慌
_ = (StringSet(nil)).Has("key") // 不會(huì)產(chǎn)生恐慌
_ = ((*Age)(nil)).IsNil() // 不會(huì)產(chǎn)生恐慌
// 下面這行將產(chǎn)生一個(gè)恐慌,但是此恐慌不是在調(diào)用方法的時(shí)
// 候產(chǎn)生的,而是在此方法體內(nèi)解引用空指針的時(shí)候產(chǎn)生的。
((*Age)(nil)).Increase()
}
和普通參數(shù)傳參一樣,屬主參數(shù)的傳參也是一個(gè)值復(fù)制過(guò)程。 所以,在方法體內(nèi)對(duì)屬主參數(shù)的直接部分的修改將不會(huì)反映到方法體外。
一個(gè)例子:
package main
import "fmt"
type Book struct {
pages int
}
func (b Book) SetPages(pages int) {
b.pages = pages
}
func main() {
var b Book
b.SetPages(123)
fmt.Println(b.pages) // 0
}
另一個(gè)例子:
package main
import "fmt"
type Book struct {
pages int
}
type Books []Book
func (books Books) Modify() {
// 對(duì)屬主參數(shù)的間接部分的修改將反映到方法之外。
books[0].pages = 500
// 對(duì)屬主參數(shù)的直接部分的修改不會(huì)反映到方法之外。
books = append(books, Book{789})
}
func main() {
var books = Books{{123}, {456}}
books.Modify()
fmt.Println(books) // [{500} {456}]
}
有點(diǎn)題外話,如果將上例中Modify
方法中的兩行代碼次序調(diào)換,那么此方法中的兩處修改都不能反映到此方法之外。
func (books Books) Modify() {
books = append(books, Book{789})
books[0].pages = 500
}
func main() {
var books = Books{{123}, {456}}
books.Modify()
fmt.Println(books) // [{123} {456}]
}
這兩處修改都不能反映到Modify
方法之外的原因是append
函數(shù)調(diào)用將開辟一塊新的內(nèi)存來(lái)存儲(chǔ)它返回的結(jié)果切片的元素。 而此結(jié)果切片的前兩個(gè)元素是屬主參數(shù)切片的元素的副本。對(duì)此副本所做的修改不會(huì)反映到Modify
方法之外。
為了將此兩處修改反映到Modify
方法之外,Modify
方法的屬主類型應(yīng)該改為指針類型:
func (books *Books) Modify() {
*books = append(*books, Book{789})
(*books)[0].pages = 500
}
func main() {
var books = Books{{123}, {456}}
books.Modify()
fmt.Println(books) // [{500} {456} {789}]
}
在編譯階段,編譯器將正規(guī)化各個(gè)方法值表達(dá)式。簡(jiǎn)而言之,正規(guī)化就是將方法值表達(dá)式中的隱式取地址和解引用操作均轉(zhuǎn)換為顯式操作。
假設(shè)值v
的類型為T
,并且v.m
是一個(gè)合法的方法值表達(dá)式,
m
是一個(gè)為類型*T
顯式聲明的方法,那么編譯器將把它正規(guī)化(&v).m
;m
是一個(gè)為類型T
顯式聲明的方法,那么v.m
已經(jīng)是一個(gè)正規(guī)化的方法值表達(dá)式。假設(shè)值p
的類型為*T
,并且p.m
是一個(gè)合法的方法值表達(dá)式,
m
是一個(gè)為類型T
顯式聲明的方法,那么編譯器將把它正規(guī)化(*p).m
;m
是一個(gè)為類型*T
顯式聲明的方法,那么p.m
已經(jīng)是一個(gè)正規(guī)化的方法值表達(dá)式。提升方法值的正規(guī)化將在隨后的類型內(nèi)嵌一文中解釋。
假設(shè)v.m
是一個(gè)已經(jīng)正規(guī)化的方法值表達(dá)式,在運(yùn)行時(shí)刻,當(dāng)v.m
被估值的時(shí)候,屬主實(shí)參v
的估值結(jié)果的一個(gè)副本將被存儲(chǔ)下來(lái)以供后面調(diào)用此方法值的時(shí)候使用。
以下面的代碼為例:
b.Pages
是一個(gè)已經(jīng)正規(guī)化的方法值表達(dá)式。 在運(yùn)行時(shí)刻對(duì)其進(jìn)行估值時(shí),屬主實(shí)參b
的一個(gè)副本將被存儲(chǔ)下來(lái)。 此副本等于b
的當(dāng)前值:Book{pages: 123}
,此后對(duì)b
值的修改不影響此副本值。 這就是為什么調(diào)用f1()
打印出123
。p.Pages
將被正規(guī)化為(*p).Pages
。 在運(yùn)行時(shí)刻,屬主實(shí)參*p
被估值為當(dāng)前的b
值,也就是Book{pages: 123}
。 這就是為什么調(diào)用f2()
也打印出123
。p.Pages2
是一個(gè)已經(jīng)正規(guī)化的方法值表達(dá)式。 在運(yùn)行時(shí)刻對(duì)其進(jìn)行估值時(shí),屬主實(shí)參p
的一個(gè)副本將被存儲(chǔ)下來(lái),此副本的值為b
值的地址。 當(dāng)b
被修改后,此修改可以通過(guò)對(duì)此地址值解引用而反映出來(lái),這就是為什么調(diào)用g1()
打印出789
。b.Pages2
將被正規(guī)化為(&b).Pages2
。 在運(yùn)行時(shí)刻,屬主實(shí)參&b
的估值結(jié)果的一個(gè)副本將被存儲(chǔ)下來(lái),此副本的值為b
值的地址。 這就是為什么調(diào)用g2()
也打印出789
。package main
import "fmt"
type Book struct {
pages int
}
func (b Book) Pages() int {
return b.pages
}
func (b *Book) Pages2() int {
return (*b).Pages()
}
func main() {
var b = Book{pages: 123}
var p = &b
var f1 = b.Pages
var f2 = p.Pages
var g1 = p.Pages2
var g2 = b.Pages2
b.pages = 789
fmt.Println(f1()) // 123
fmt.Println(f2()) // 123
fmt.Println(g1()) // 789
fmt.Println(g2()) // 789
}
舉個(gè)例子,在下面的代碼中,定義類型Age
并不像它的源類型MyInt
一樣擁有一個(gè)IsOdd
方法。
package main
type MyInt int
func (mi MyInt) IsOdd() bool {
return mi%2 == 1
}
type Age MyInt
func main() {
var x MyInt = 3
_ = x.IsOdd() // okay
var y Age = 36
// _ = y.IsOdd() // error: y.IsOdd undefined
_ = y
}
首先,從上一節(jié)中的例子,我們可以得知有時(shí)候我們必須在某些方法聲明中使用指針類型屬主。
事實(shí)上,我們總可以在方法聲明中使用指針類型屬主而不會(huì)產(chǎn)生任何邏輯問(wèn)題。 我們僅僅是為了程序效率考慮有時(shí)候才會(huì)在函數(shù)聲明中使用值類型屬主。
對(duì)于值類型屬主還是指針類型屬主都可以接受的方法聲明,下面列出了一些考慮因素:
sync
標(biāo)準(zhǔn)庫(kù)包中的類型的值不應(yīng)該被復(fù)制,所以如果一個(gè)結(jié)構(gòu)體類型內(nèi)嵌了這些類型,則不應(yīng)該為這個(gè)結(jié)構(gòu)體類型聲明值類型屬主的方法。
如果實(shí)在拿不定主意在一個(gè)方法聲明中應(yīng)該使用值類型屬主還是指針類型屬主,那么請(qǐng)使用指針類型屬主。
更多建議: