淺析Go語言中的方法集合與選擇receiver類型
一、receiver 參數(shù)類型對 Go 方法的影響
要想為 receiver
參數(shù)選出合理的類型,我們先要了解不同的 receiver
參數(shù)類型會對 Go 方法產(chǎn)生怎樣的影響。其實,Go 方法實質(zhì)上是以方法的 receiver
參數(shù)作為第一個參數(shù)的普通函數(shù)。
對于函數(shù)參數(shù)類型對函數(shù)的影響,我們是很熟悉的。那么我們能不能將方法等價轉(zhuǎn)換為對應(yīng)的函數(shù),再通過分析 receiver
參數(shù)類型對函數(shù)的影響,從而間接得出它對 Go 方法的影響呢?
基于這個思路。我們直接來看下面例子中的兩個 Go 方法,以及它們等價轉(zhuǎn)換后的函數(shù):
func (t T) M1() <=> F1(t T) func (t *T) M2() <=> F2(t *T)
這個例子中有方法 M1
和 M2
。M1
方法是 receiver 參數(shù)類型為 T
的一類方法的代表,而 M2
方法則代表了 receiver 參數(shù)類型為 *T
的另一類。下面我們分別來看看不同的 receiver
參數(shù)類型對 M1
和 M2
的影響。
首先,當(dāng) receiver
參數(shù)的類型為 T
時:當(dāng)我們選擇以 T
作為 receiver
參數(shù)類型時,M1
方法等價轉(zhuǎn)換為 F1(t T)
。我們知道,Go 函數(shù)的參數(shù)采用的是值拷貝傳遞,也就是說,F1
函數(shù)體中的 t
是 T
類型實例的一個副本。這樣,我們在 F1
函數(shù)的實現(xiàn)中對參數(shù) t
做任何修改,都只會影響副本,而不會影響到原 T
類型實例。
據(jù)此我們可以得出結(jié)論:當(dāng)我們的方法 M1
采用類型為 T
的 receiver
參數(shù)時,代表 T
類型實例的 receiver
參數(shù)以值傳遞方式傳遞到 M1
方法體中的,實際上是 T
類型實例的副本,M1
方法體中對副本的任何修改操作,都不會影響到原 T
類型實例。
第二,當(dāng) receiver
參數(shù)的類型為 *T
時:當(dāng)我們選擇以 *T
作為 receiver
參數(shù)類型時,M2
方法等價轉(zhuǎn)換為 F2(t *T)
。同上面分析,我們傳遞給 F2
函數(shù)的 t
是 T
類型實例的地址,這樣 F2
函數(shù)體中對參數(shù) t
做的任何修改,都會反映到原 T
類型實例上。
據(jù)此我們也可以得出結(jié)論:當(dāng)我們的方法 M2
采用類型為 *T
的 receiver
參數(shù)時,代表 *T
類型實例的 receiver
參數(shù)以值傳遞方式傳遞到 M2
方法體中的,實際上是 T
類型實例的地址,M2
方法體通過該地址可以對原 T
類型實例進(jìn)行任何修改操作。
我們再通過一個更直觀的例子,證明一下上面這個分析結(jié)果,看一下 Go 方法選擇不同的 receiver
類型對原類型實例的影響:
package main type T struct { a int } func (t T) M1() { t.a = 10 } func (t *T) M2() { t.a = 11 } func main() { var t T println(t.a) // 0 t.M1() println(t.a) // 0 p := &t p.M2() println(t.a) // 11 }
在這個示例中,我們?yōu)榛愋?nbsp;T
定義了兩個方法 M1
和 M2
,其中 M1
的 receiver
參數(shù)類型為 T
,而 M2
的 receiver
參數(shù)類型為 *T
。M1
和 M2
方法體都通過 receiver
參數(shù) t
對 t
的字段 a
進(jìn)行了修改。
但運(yùn)行這個示例程序后,我們看到,方法 M1
由于使用了 T
作為 receiver
參數(shù)類型,它在方法體中修改的僅僅是 T
類型實例 t
的副本,原實例并沒有受到影響。因此 M1
調(diào)用后,輸出 t.a
的值仍為 0。
而方法 M2
呢,由于使用了 *T
作為 receiver
參數(shù)類型,它在方法體中通過 t
修改的是實例本身,因此 M2
調(diào)用后,t.a
的值變?yōu)榱?11,這些輸出結(jié)果與我們前面的分析是一致的。
二、選擇 receiver 參數(shù)類型原則
2.1 選擇 receiver 參數(shù)類型的第一個原則
基于上面的影響分析,我們可以得到選擇 receiver
參數(shù)類型的第一個原則:如果 Go 方法要把對 receiver
參數(shù)代表的類型實例的修改,反映到原類型實例上,那么我們應(yīng)該選擇 *T
作為 receiver
參數(shù)的類型。
可能會有個疑問:如果我們選擇了 *T
作為 Go 方法 receiver
參數(shù)的類型,那么我們是不是只能通過 *T
類型變量調(diào)用該方法,而不能通過 T
類型變量調(diào)用了呢?我們改造上面例子看一下:
type T struct { a int } func (t T) M1() { t.a = 10 } func (t *T) M2() { t.a = 11 } func main() { var t1 T println(t1.a) // 0 t1.M1() println(t1.a) // 0 t1.M2() println(t1.a) // 11 var t2 = &T{} println(t2.a) // 0 t2.M1() println(t2.a) // 0 t2.M2() println(t2.a) // 11 }
我們先來看看類型為 T
的實例 t1
。我們看到它不僅可以調(diào)用 receiver
參數(shù)類型為 T
的方法 M1
,它還可以直接調(diào)用 receiver
參數(shù)類型為 *T
的方法 M2
,并且調(diào)用完 M2
方法后,t1.a
的值被修改為 11 了。
其實,T
類型的實例 t1
之所以可以調(diào)用 receiver
參數(shù)類型為 *T
的方法 M2
,都是 Go 編譯器在背后自動進(jìn)行轉(zhuǎn)換的結(jié)果?;蛘哒f,t1.M2()
這種用法是 Go 提供的“語法糖”:Go 判斷 t1
的類型為 T
,也就是與方法 M2
的 receiver
參數(shù)類型 *T
不一致后,會自動將 t1.M2()
轉(zhuǎn)換為 (&t1).M2()
。
同理,類型為 *T
的實例 t2
,它不僅可以調(diào)用 receiver
參數(shù)類型為 *T
的方法 M2
,還可以調(diào)用 receiver
參數(shù)類型為 T
的方法 M1
,這同樣是因為 Go 編譯器在背后做了轉(zhuǎn)換。也就是,Go 判斷 t2
的類型為 *T
,與方法 M1
的 receiver
參數(shù)類型 T
不一致,就會自動將 t2.M1()
轉(zhuǎn)換為 (*t2).M1()
。
通過這個實例,我們知道了這樣一個結(jié)論:無論是 T
類型實例,還是 *T
類型實例,都既可以調(diào)用 receiver
為 T
類型的方法,也可以調(diào)用 receiver
為 *T
類型的方法。這樣,我們在為方法選擇 receiver
參數(shù)的類型的時候,就不需要擔(dān)心這個方法不能被與 receiver
參數(shù)類型不一致的類型實例調(diào)用了。
2.2 選擇 receiver 參數(shù)類型的第二個原則
前面我們第一個原則說的是,當(dāng)我們要在方法中對 receiver
參數(shù)代表的類型實例進(jìn)行修改,那我們要為 receiver
參數(shù)選擇 *T
類型,但是如果我們不需要在方法中對類型實例進(jìn)行修改呢?這個時候我們是為 receiver
參數(shù)選擇 T
類型還是 *T
類型呢?
這也得分情況。一般情況下,我們通常會為 receiver
參數(shù)選擇 T
類型,因為這樣可以縮窄外部修改類型實例內(nèi)部狀態(tài)的“接觸面”,也就是盡量少暴露可以修改類型內(nèi)部狀態(tài)的方法。
不過也有一個例外需要你特別注意。考慮到 Go 方法調(diào)用時,receiver
參數(shù)是以值拷貝的形式傳入方法中的。那么,如果 receiver
參數(shù)類型的 size 較大,以值拷貝形式傳入就會導(dǎo)致較大的性能開銷,這時我們選擇 *T
作為 receiver
類型可能更好些。
以上這些可以作為我們選擇 receiver
參數(shù)類型的第二個原則。
三、方法集合(Method Set)
3.1 引入
我們先通過一個示例,直觀了解一下為什么要有方法集合,它主要用來解決什么問題:
type Interface interface { M1() M2() } type T struct{} func (t T) M1() {} func (t *T) M2() {} func main() { var t T var pt *T var i Interface i = pt i = t // cannot use t (type T) as type Interface in assignment: T does not implement Interface (M2 method has pointer receiver) }
在這個例子中,我們定義了一個接口類型 Interface
以及一個自定義類型 T
。Interface
接口類型包含了兩個方法 M1
和 M2
,代碼中還定義了基類型為 T
的兩個方法 M1
和 M2
,但它們的 receiver
參數(shù)類型不同,一個為 T
,另一個為 *T
。在 main
函數(shù)中,我們分別將 T
類型實例 t
和 *T
類型實例 pt
賦值給 Interface
類型變量 i
。
運(yùn)行一下這個示例程序,我們在 i = t
這一行會得到 Go 編譯器的錯誤提示,Go 編譯器提示我們:T
沒有實現(xiàn) Interface
類型方法列表中的 M2
,因此類型 T
的實例 t
不能賦值給 Interface
變量。
可是,為什么呢?為什么 *T
類型的 pt
可以被正常賦值給 Interface
類型變量 i
,而 T
類型的 t
就不行呢?如果說 T
類型是因為只實現(xiàn)了 M1
方法,未實現(xiàn) M2
方法而不滿足 Interface
類型的要求,那么 *T
類型也只是實現(xiàn)了 M2
方法,并沒有實現(xiàn) M1
方法???
有些事情并不是表面看起來這個樣子的。了解方法集合后,這個問題就迎刃而解了。同時,方法集合也是用來判斷一個類型是否實現(xiàn)了某接口類型的唯一手段,可以說,“方法集合決定了接口實現(xiàn)”。
3.2 類型的方法集合
Go 中任何一個類型都有屬于自己的方法集合,或者說方法集合是 Go 類型的一個“屬性”。但不是所有類型都有自巴基斯坦的方法呀,比如 int
類型就沒有。所以,對于沒有定義方法的 Go 類型,我們稱其擁有空方法集合。
接口類型相對特殊,它只會列出代表接口的方法列表,不會具體定義某個方法,它的方法集合就是它的方法列表中的所有方法,我們可以一目了然地看到。
為了方便查看一個非接口類型的方法集合,這里提供了一個函數(shù) dumpMethodSet
,用于輸出一個非接口類型的方法集合:
func dumpMethodSet(i interface{}) { dynTyp := reflect.TypeOf(i) if dynTyp == nil { fmt.Printf("there is no dynamic type\n") return } n := dynTyp.NumMethod() if n == 0 { fmt.Printf("%s's method set is empty!\n", dynTyp) return } fmt.Printf("%s's method set:\n", dynTyp) for j := 0; j < n; j++ { fmt.Println("-", dynTyp.Method(j).Name) } fmt.Printf("\n") }
下面我們利用這個函數(shù),試著輸出一下 Go 原生類型以及自定義類型的方法集合,看下面代碼:
type T struct{} func (T) M1() {} func (T) M2() {} func (*T) M3() {} func (*T) M4() {} func main() { var n int dumpMethodSet(n) dumpMethodSet(&n) var t T dumpMethodSet(t) dumpMethodSet(&t) }
運(yùn)行這段代碼,我們得到如下結(jié)果:
int's method set is empty!
*int's method set is empty!
main.T's method set:
- M1
- M2
*main.T's method set:
- M1
- M2
- M3
- M4
我們看到以 int
、*int
為代表的 Go 原生類型由于沒有定義方法,所以它們的方法集合都是空的。自定義類型 T
定義了方法 M1
和 M2
,因此它的方法集合包含了 M1
和 M2
,也符合我們預(yù)期。但 *T
的方法集合中除了預(yù)期的 M3
和 M4
之外,居然還包含了類型 T
的方法 M1
和 M2
!
不過,這里程序的輸出并沒有錯誤。
這是因為,Go 語言規(guī)定,*T
類型的方法集合包含所有以 *T
為 receiver
參數(shù)類型的方法,以及所有以 T
為 receiver
參數(shù)類型的方法。這就是這個示例中為何 *T
類型的方法集合包含四個方法的原因。
這個時候,你是不是也找到了前面那個示例中為何 i = pt
沒有報編譯錯誤的原因了呢?我們同樣可以使用 dumpMethodSet
工具函數(shù),輸出一下那個例子中 pt
與 t
各自所屬類型的方法集合:
type Interface interface { M1() M2() } type T struct{} func (t T) M1() {} func (t *T) M2() {} func main() { var t T var pt *T dumpMethodSet(t) dumpMethodSet(pt) }
運(yùn)行上述代碼,我們得到如下結(jié)果:
main.T's method set:
- M1
*main.T's method set:
- M1
- M2
通過這個輸出結(jié)果,我們可以一目了然地看到 T
、*T
各自的方法集合。
我們看到,T
類型的方法集合中只包含 M1
,沒有 Interface
類型方法集合中的 M2
方法,這就是 Go 編譯器認(rèn)為變量 t
不能賦值給 Interface
類型變量的原因
在輸出的結(jié)果中,我們還看到 *T
類型的方法集合除了包含它自身定義的 M2
方法外,還包含了 T
類型定義的 M1
方法,*T
的方法集合與 Interface
接口類型的方法集合是一樣的,因此 pt
可以被賦值給 Interface
接口類型的變量 i
。
到這里,我們已經(jīng)知道了所謂的方法集合決定接口實現(xiàn)的含義就是:如果某類型 T
的方法集合與某接口類型的方法集合相同,或者類型 T
的方法集合是接口類型 I
方法集合的超集,那么我們就說這個類型 T
實現(xiàn)了接口 I
?;蛘哒f,方法集合這個概念在 Go 語言中的主要用途,就是用來判斷某個類型是否實現(xiàn)了某個接口。
四、選擇 receiver 參數(shù)類型的第三個原則
理解了方法集合后,我們再理解第三個原則的內(nèi)容就不難了。這個原則的選擇依據(jù)就是 T 類型是否需要實現(xiàn)某個接口,也就是是否存在將 T 類型的變量賦值給某接口類型變量的情況。
理解了方法集合后,我們再理解第三個原則的內(nèi)容就不難了。這個原則的選擇依據(jù)就是 T
類型是否需要實現(xiàn)某個接口,也就是是否存在將 T
類型的變量賦值給某接口類型變量的情況。
如果 T
類型需要實現(xiàn)某個接口,那我們就要使用 T
作為 receiver
參數(shù)的類型,來滿足接口類型方法集合中的所有方法。
如果 T
不需要實現(xiàn)某一接口,但 *T
需要實現(xiàn)該接口,那么根據(jù)方法集合概念,*T
的方法集合是包含 T
的方法集合的,這樣我們在確定 Go 方法的 receiver
的類型時,參考原則一和原則二就可以了。
如果說前面的兩個原則更多聚焦于類型內(nèi)部,從單個方法的實現(xiàn)層面考慮,那么這第三個原則則是更多從全局的設(shè)計層面考慮,聚焦于這個類型與接口類型間的耦合關(guān)系。
五、小結(jié)
在實際進(jìn)行 Go 方法設(shè)計時,我們首先應(yīng)該考慮的是原則三,即 T 類型是否要實現(xiàn)某一接口。如果 T 類型需要實現(xiàn)某一接口的全部方法,那么我們就需要使用 T 作為 receiver 參數(shù)的類型來滿足接口類型方法集合中的所有方法。
如果 T
類型不需要實現(xiàn)某一接口,那么我們就可以參考原則一和原則二來為 receiver
參數(shù)選擇類型了。也就是,如果 Go 方法要把對 receiver
參數(shù)所代表的類型實例的修改反映到原類型實例上,那么我們應(yīng)該選擇 *T
作為 receiver
參數(shù)的類型。否則通常我們會為 receiver
參數(shù)選擇 T
類型,這樣可以減少外部修改類型實例內(nèi)部狀態(tài)的“渠道”。除非 receiver
參數(shù)類型的 size
較大,考慮到傳值的較大性能開銷,選擇 *T
作為 receiver
類型可能更適合。
方法集合在 Go 語言中的主要用途就是判斷某個類型是否實現(xiàn)了某個接口。方法集合像“膠水”一樣,將自定義類型與接口隱式地“粘結(jié)”在一起
以上就是淺析Go語言中的方法集合與選擇receiver類型的詳細(xì)內(nèi)容,更多關(guān)于Go方法集合與receiver的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang中interface轉(zhuǎn)string輸出打印方法
這篇文章主要給大家介紹了關(guān)于Golang中interface轉(zhuǎn)string輸出打印的相關(guān)資料,在go語言中interface轉(zhuǎn)string可以直接使用fmt提供的fmt函數(shù),文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-02-02Golang基礎(chǔ)學(xué)習(xí)之map的示例詳解
哈希表是常見的數(shù)據(jù)結(jié)構(gòu),有的語言會將哈希稱作字典或者映射,在Go中,哈希就是常見的數(shù)據(jù)類型map,本文就來聊聊Golang中map的相關(guān)知識吧2023-03-03