Go語(yǔ)言 方法

2023-02-16 17:38 更新

Go支持一些面向?qū)ο缶幊烫匦?,方法是這些所支持的特性之一。 本篇文章將介紹在Go中和方法相關(guān)的各種概念。

方法聲明

在Go中,我們可以為類型T*T顯式地聲明一個(gè)方法,其中類型T必須滿足四個(gè)條件:

  1. T必須是一個(gè)定義類型;
  2. T必須和此方法聲明定義在同一個(gè)代碼包中;
  3. T不能是一個(gè)指針類型;
  4. T不能是一個(gè)接口類型。接口類型將在下一篇文章中講解。

類型T*T稱為它們各自的方法的屬主類型(receiver type)。 類型T被稱作為類型T*T聲明的所有方法的屬主基類型(receiver base type)。

注意:我們也可以為滿足上列條件的類型T*T別名聲明方法。 這樣做的效果和直接為類型T*T聲明方法是一樣的。

如果我們?yōu)槟硞€(gè)類型聲明了一個(gè)方法,以后我們可以說(shuō)此類型擁有此方法。

從上面列出的條件,我們得知我們不能為下列類型(顯式地)聲明方法:

  • 內(nèi)置基本類型。比如intstring。 因?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é)中介紹。

每個(gè)方法對(duì)應(yīng)著一個(gè)隱式聲明的函數(shù)

對(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)聲明。

方法描述(method specification)和方法集(method set)

一個(gè)方法描述可以看作是一個(gè)不帶func關(guān)鍵字的函數(shù)原型。 我們可以把每個(gè)方法聲明看作是由一個(gè)func關(guān)鍵字、一個(gè)屬主參數(shù)聲明部分、一個(gè)方法描述和一個(gè)方法體組成。

比如,上面的例子中的PagesSetPages的描述如下:

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)將在下一篇文章中講解。

下列類型的方法集總為空:

  • 內(nèi)置基本類型;
  • 定義的指針類型;
  • 基類型為指針類型或者接口類型的指針類型;
  • 無(wú)名數(shù)組/切片/映射/函數(shù)/通道類型。

方法值和方法調(diào)用

方法事實(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ù)的傳參是一個(gè)值復(fù)制過(guò)程

和普通參數(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ī)化

在編譯階段,編譯器將正規(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。
  • 在編譯時(shí)刻,方法值表達(dá)式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。
  • 在編譯時(shí)刻,方法值表達(dá)式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è)定義類型不會(huì)獲取為它的源類型顯式聲明的方法

舉個(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
}

如何決定一個(gè)方法聲明使用值類型屬主還是指針類型屬主?

首先,從上一節(jié)中的例子,我們可以得知有時(shí)候我們必須在某些方法聲明中使用指針類型屬主。

事實(shí)上,我們總可以在方法聲明中使用指針類型屬主而不會(huì)產(chǎn)生任何邏輯問(wèn)題。 我們僅僅是為了程序效率考慮有時(shí)候才會(huì)在函數(shù)聲明中使用值類型屬主。

對(duì)于值類型屬主還是指針類型屬主都可以接受的方法聲明,下面列出了一些考慮因素:

  • 太多的指針可能會(huì)增加垃圾回收器的負(fù)擔(dān)。
  • 如果一個(gè)值類型的尺寸太大,那么屬主參數(shù)在傳參的時(shí)候的復(fù)制成本將不可忽略。 指針類型都是小尺寸類型。 關(guān)于各種不同類型的尺寸,請(qǐng)閱讀值復(fù)制代價(jià)一文。
  • 在并發(fā)場(chǎng)合下,同時(shí)調(diào)用值類型屬主和指針類型屬主方法比較易于產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)。
  • 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)使用指針類型屬主。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)