golang 接口类型 interface 简介使用
1. Go 語言與鴨子類型的關(guān)系
先直接來看維基百科里的定義:
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
翻譯過來就是:如果某個(gè)東西長得像鴨子,像鴨子一樣游泳,像鴨子一樣嘎嘎叫,那它就可以被看成是一只鴨子。
Duck Typing,鴨子類型,是動(dòng)態(tài)編程語言的一種對(duì)象推斷策略,它更關(guān)注對(duì)象能如何被使用,而不是對(duì)象的類型本身。Go 語言作為一門靜態(tài)語言,它通過通過接口的方式完美支持鴨子類型。
例如,在動(dòng)態(tài)語言 python 中,定義一個(gè)這樣的函數(shù):
def hello_world(coder):coder.say_hello()當(dāng)調(diào)用此函數(shù)的時(shí)候,可以傳入任意類型,只要它實(shí)現(xiàn)了?say_hello()?函數(shù)就可以。如果沒有實(shí)現(xiàn),運(yùn)行過程中會(huì)出現(xiàn)錯(cuò)誤。
而在靜態(tài)語言如 Java, C++ 中,必須要顯示地聲明實(shí)現(xiàn)了某個(gè)接口,之后,才能用在任何需要這個(gè)接口的地方。如果你在程序中調(diào)用?hello_world?函數(shù),卻傳入了一個(gè)根本就沒有實(shí)現(xiàn)?say_hello()?的類型,那在編譯階段就不會(huì)通過。這也是靜態(tài)語言比動(dòng)態(tài)語言更安全的原因。
動(dòng)態(tài)語言和靜態(tài)語言的差別在此就有所體現(xiàn)。靜態(tài)語言在編譯期間就能發(fā)現(xiàn)類型不匹配的錯(cuò)誤,不像動(dòng)態(tài)語言,必須要運(yùn)行到那一行代碼才會(huì)報(bào)錯(cuò)。插一句,這也是我不喜歡用?python?的一個(gè)原因。當(dāng)然,靜態(tài)語言要求程序員在編碼階段就要按照規(guī)定來編寫程序,為每個(gè)變量規(guī)定數(shù)據(jù)類型,這在某種程度上,加大了工作量,也加長了代碼量。動(dòng)態(tài)語言則沒有這些要求,可以讓人更專注在業(yè)務(wù)上,代碼也更短,寫起來更快,這一點(diǎn),寫 python 的同學(xué)比較清楚。
Go 語言作為一門現(xiàn)代靜態(tài)語言,是有后發(fā)優(yōu)勢的。它引入了動(dòng)態(tài)語言的便利,同時(shí)又會(huì)進(jìn)行靜態(tài)語言的類型檢查,寫起來是非常 Happy 的。Go 采用了折中的做法:不要求類型顯示地聲明實(shí)現(xiàn)了某個(gè)接口,只要實(shí)現(xiàn)了相關(guān)的方法即可,編譯器就能檢測到。
來看個(gè)例子:
先定義一個(gè)接口,和使用此接口作為參數(shù)的函數(shù):
type IGreeting interface {sayHello() }func sayHello(i IGreeting) {i.sayHello() }再來定義兩個(gè)結(jié)構(gòu)體:
type Go struct {} func (g Go) sayHello() {fmt.Println("Hi, I am GO!") }type PHP struct {} func (p PHP) sayHello() {fmt.Println("Hi, I am PHP!") }最后,在 main 函數(shù)里調(diào)用 sayHello() 函數(shù):
func main() {golang := Go{}php := PHP{}sayHello(golang)sayHello(php) }程序輸出:
Hi, I am GO! Hi, I am PHP!在 main 函數(shù)中,調(diào)用?sayHello() 函數(shù)時(shí),傳入了?golang, php?對(duì)象,它們并沒有顯式地聲明實(shí)現(xiàn)了 IGreeting 類型,只是實(shí)現(xiàn)了接口所規(guī)定的 sayHello() 函數(shù)。實(shí)際上,編譯器在調(diào)用 sayHello() 函數(shù)時(shí),會(huì)隱式地將?golang, php?對(duì)象轉(zhuǎn)換成 IGreeting 類型,這也是靜態(tài)語言的類型檢查功能。
順帶再提一下動(dòng)態(tài)語言的特點(diǎn):
變量綁定的類型是不確定的,在運(yùn)行期間才能確定
函數(shù)和方法可以接收任何類型的參數(shù),且調(diào)用時(shí)不檢查參數(shù)類型
不需要實(shí)現(xiàn)接口
總結(jié)一下,鴨子類型是一種動(dòng)態(tài)語言的風(fēng)格,在這種風(fēng)格中,一個(gè)對(duì)象有效的語義,不是由繼承自特定的類或?qū)崿F(xiàn)特定的接口,而是由它”當(dāng)前方法和屬性的集合”決定。Go 作為一種靜態(tài)語言,通過接口實(shí)現(xiàn)了?鴨子類型,實(shí)際上是 Go 的編譯器在其中作了隱匿的轉(zhuǎn)換工作。
2. 值接收者和指針接收者的區(qū)別
方法
方法能給用戶自定義的類型添加新的行為。它和函數(shù)的區(qū)別在于方法有一個(gè)接收者,給一個(gè)函數(shù)添加一個(gè)接收者,那么它就變成了方法。接收者可以是值接收者,也可以是指針接收者。
在調(diào)用方法的時(shí)候,值類型既可以調(diào)用值接收者的方法,也可以調(diào)用指針接收者的方法;指針類型既可以調(diào)用指針接收者的方法,也可以調(diào)用值接收者的方法。
也就是說,不管方法的接收者是什么類型,該類型的值和指針都可以調(diào)用,不必嚴(yán)格符合接收者的類型。
來看個(gè)例子:
package mainimport "fmt"type Person struct {age int }func (p Person) howOld() int {return p.age }func (p *Person) growUp() {p.age += 1 }func main() {// qcrao 是值類型qcrao := Person{age: 18}// 值類型 調(diào)用接收者也是值類型的方法fmt.Println(qcrao.howOld())// 值類型 調(diào)用接收者是指針類型的方法qcrao.growUp()fmt.Println(qcrao.howOld())// ----------------------// stefno 是指針類型stefno := &Person{age: 100}// 指針類型 調(diào)用接收者是值類型的方法fmt.Println(stefno.howOld())// 指針類型 調(diào)用接收者也是指針類型的方法stefno.growUp()fmt.Println(stefno.howOld()) }上例子的輸出結(jié)果是:
18 19 100 101調(diào)用了?growUp?函數(shù)后,不管調(diào)用者是值類型還是指針類型,它的?Age?值都改變了。
實(shí)際上,當(dāng)類型和方法的接收者類型不同時(shí),其實(shí)是編譯器在背后做了一些工作,用一個(gè)表格來呈現(xiàn):
| 值類型調(diào)用者 | 方法會(huì)使用調(diào)用者的一個(gè)副本,類似于“傳值” | 使用值的引用來調(diào)用方法,上例中,qcrao.growUp()?實(shí)際上是?(&qcrao).growUp() |
| 指針類型調(diào)用者 | 指針被解引用為值,上例中,stefno.howOld()?實(shí)際上是?(*stefno).howOld() | 實(shí)際上也是“傳值”,方法里的操作會(huì)影響到調(diào)用者,類似于指針傳參,拷貝了一份指針 |
值接收者和指針接收者
前面說過,不管接收者類型是值類型還是指針類型,都可以通過值類型或指針類型調(diào)用,這里面實(shí)際上通過語法糖起作用的。
先說結(jié)論:實(shí)現(xiàn)了接收者是值類型的方法,相當(dāng)于自動(dòng)實(shí)現(xiàn)了接收者是指針類型的方法;而實(shí)現(xiàn)了接收者是指針類型的方法,不會(huì)自動(dòng)生成對(duì)應(yīng)接收者是值類型的方法。
來看一個(gè)例子,就會(huì)完全明白:
package mainimport "fmt"type coder interface {code()debug() }type Gopher struct {language string }func (p Gopher) code() {fmt.Printf("I am coding %s language\n", p.language) }func (p *Gopher) debug() {fmt.Printf("I am debuging %s language\n", p.language) }func main() {var c coder = &Gopher{"Go"}c.code()c.debug() }上述代碼里定義了一個(gè)接口?coder,接口定義了兩個(gè)函數(shù):
code() debug()接著定義了一個(gè)結(jié)構(gòu)體?Gopher,它實(shí)現(xiàn)了兩個(gè)方法,一個(gè)值接收者,一個(gè)指針接收者。
最后,我們在?main?函數(shù)里通過接口類型的變量調(diào)用了定義的兩個(gè)函數(shù)。
運(yùn)行一下,結(jié)果:
I am coding Go language I am debuging Go language但是如果我們把?main?函數(shù)的第一條語句換一下:
func main() {var c coder = Gopher{"Go"}c.code()c.debug() }運(yùn)行一下,報(bào)錯(cuò):
./main.go:23:6: cannot use Gopher literal (type Gopher) as type coder in assignment:Gopher does not implement coder (debug method has pointer receiver)看出這兩處代碼的差別了嗎?第一次是將?&Gopher?賦給了?coder;第二次則是將?Gopher?賦給了?coder。
第二次報(bào)錯(cuò)是說,Gopher?沒有實(shí)現(xiàn)?coder,很明顯了吧?因?yàn)?Gopher?類型并沒有實(shí)現(xiàn)?debug?方法。表面上看,?*Gopher?類型也沒有實(shí)現(xiàn)?code?方法,但是因?yàn)?Gopher?類型實(shí)現(xiàn)了?code?方法,所以讓?*Gopher?類型自動(dòng)擁有了?code?方法。
當(dāng)然,上面的說法有一個(gè)簡單的解釋:接收者是指針類型的方法,很可能在方法中會(huì)對(duì)接收者的屬性進(jìn)行更改操作,從而影響接收者;而對(duì)于接收者是值類型的方法,在方法中不會(huì)對(duì)接收者本身產(chǎn)生影響。
所以,當(dāng)實(shí)現(xiàn)了一個(gè)接收者是值類型的方法,就可以自動(dòng)生成一個(gè)接收者是對(duì)應(yīng)指針類型的方法,因?yàn)閮烧叨疾粫?huì)影響接收者。但是,當(dāng)實(shí)現(xiàn)了一個(gè)接收者是指針類型的方法,如果此時(shí)自動(dòng)生成一個(gè)接收者是值類型的方法,原本期望對(duì)接收者的改變(通過指針實(shí)現(xiàn)),現(xiàn)在無法實(shí)現(xiàn),因?yàn)橹殿愋蜁?huì)產(chǎn)生一個(gè)拷貝,不會(huì)真正影響調(diào)用者。
最后,只要記住下面這點(diǎn)就可以了:
如果實(shí)現(xiàn)了接收者是值類型的方法,會(huì)隱含地也實(shí)現(xiàn)了接收者是指針類型的方法。
兩者分別在何時(shí)使用
如果方法的接收者是值類型,無論調(diào)用者是對(duì)象還是對(duì)象指針,修改的都是對(duì)象的副本,不影響調(diào)用者;如果方法的接收者是指針類型,則調(diào)用者修改的是指針指向的對(duì)象本身。
使用指針作為方法的接收者的理由:
- 方法能夠修改接收者指向的值。
- 避免在每次調(diào)用方法時(shí)復(fù)制該值,在值的類型為大型結(jié)構(gòu)體時(shí),這樣做會(huì)更加高效。
是使用值接收者還是指針接收者,不是由該方法是否修改了調(diào)用者(也就是接收者)來決定,而是應(yīng)該基于該類型的本質(zhì)。
如果類型具備“原始的本質(zhì)”,也就是說它的成員都是由 Go 語言里內(nèi)置的原始類型,如字符串,整型值等,那就定義值接收者類型的方法。像內(nèi)置的引用類型,如 slice,map,interface,channel,這些類型比較特殊,聲明他們的時(shí)候,實(shí)際上是創(chuàng)建了一個(gè)?header, 對(duì)于他們也是直接定義值接收者類型的方法。這樣,調(diào)用函數(shù)時(shí),是直接 copy 了這些類型的?header,而?header?本身就是為復(fù)制設(shè)計(jì)的。
如果類型具備非原始的本質(zhì),不能被安全地復(fù)制,這種類型總是應(yīng)該被共享,那就定義指針接收者的方法。比如 go 源碼里的文件結(jié)構(gòu)體(struct File)就不應(yīng)該被復(fù)制,應(yīng)該只有一份實(shí)體。
這一段說的比較繞,大家可以去看《Go 語言實(shí)戰(zhàn)》5.3 那一節(jié)。
3. iface 和 eface 的區(qū)別是什么
iface?和?eface?都是 Go 中描述接口的底層結(jié)構(gòu)體,區(qū)別在于?iface?描述的接口包含方法,而?eface?則是不包含任何方法的空接口:interface{}。
從源碼層面看一下:
type iface struct {tab *itabdata unsafe.Pointer }type itab struct {inter *interfacetype_type *_typelink *itabhash uint32 // copy of _type.hash. Used for type switches.bad bool // type does not implement interfaceinhash bool // has this itab been added to hash?unused [2]bytefun [1]uintptr // variable sized }iface?內(nèi)部維護(hù)兩個(gè)指針,tab?指向一個(gè)?itab?實(shí)體, 它表示接口的類型以及賦給這個(gè)接口的實(shí)體類型。data?則指向接口具體的值,一般而言是一個(gè)指向堆內(nèi)存的指針。
再來仔細(xì)看一下?itab?結(jié)構(gòu)體:_type?字段描述了實(shí)體的類型,包括內(nèi)存對(duì)齊方式,大小等;inter?字段則描述了接口的類型。fun?字段放置和接口方法對(duì)應(yīng)的具體數(shù)據(jù)類型的方法地址,實(shí)現(xiàn)接口調(diào)用方法的動(dòng)態(tài)分派,一般在每次給接口賦值發(fā)生轉(zhuǎn)換時(shí)會(huì)更新此表,或者直接拿緩存的 itab。
這里只會(huì)列出實(shí)體類型和接口相關(guān)的方法,實(shí)體類型的其他方法并不會(huì)出現(xiàn)在這里。如果你學(xué)過 C++ 的話,這里可以類比虛函數(shù)的概念。
另外,你可能會(huì)覺得奇怪,為什么?fun?數(shù)組的大小為 1,要是接口定義了多個(gè)方法可怎么辦?實(shí)際上,這里存儲(chǔ)的是第一個(gè)方法的函數(shù)指針,如果有更多的方法,在它之后的內(nèi)存空間里繼續(xù)存儲(chǔ)。從匯編角度來看,通過增加地址就能獲取到這些函數(shù)指針,沒什么影響。順便提一句,這些方法是按照函數(shù)名稱的字典序進(jìn)行排列的。
再看一下?interfacetype?類型,它描述的是接口的類型:
type interfacetype struct {typ _typepkgpath namemhdr []imethod }可以看到,它包裝了?_type?類型,_type?實(shí)際上是描述 Go 語言中各種數(shù)據(jù)類型的結(jié)構(gòu)體。我們注意到,這里還包含一個(gè)?mhdr?字段,表示接口所定義的函數(shù)列表,?pkgpath?記錄定義了接口的包名。
這里通過一張圖來看下?iface?結(jié)構(gòu)體的全貌:
iface 結(jié)構(gòu)體全景
接著來看一下?eface?的源碼:
type eface struct {_type *_typedata unsafe.Pointer }相比?iface,eface?就比較簡單了。只維護(hù)了一個(gè)?_type?字段,表示空接口所承載的具體的實(shí)體類型。data?描述了具體的值。
eface 結(jié)構(gòu)體全景
我們來看個(gè)例子:
package mainimport "fmt"func main() {x := 200var any interface{} = xfmt.Println(any)g := Gopher{"Go"}var c coder = gfmt.Println(c) }type coder interface {code()debug() }type Gopher struct {language string }func (p Gopher) code() {fmt.Printf("I am coding %s language\n", p.language) }func (p Gopher) debug() {fmt.Printf("I am debuging %s language\n", p.language) }執(zhí)行命令,打印出匯編語言:
go tool compile -S ./src/main.go可以看到,main 函數(shù)里調(diào)用了兩個(gè)函數(shù):
func convT2E64(t *_type, elem unsafe.Pointer) (e eface) func convT2I(tab *itab, elem unsafe.Pointer) (i iface)上面兩個(gè)函數(shù)的參數(shù)和?iface?及?eface?結(jié)構(gòu)體的字段是可以聯(lián)系起來的:兩個(gè)函數(shù)都是將參數(shù)組裝一下,形成最終的接口。
作為補(bǔ)充,我們最后再來看下?_type?結(jié)構(gòu)體:
type _type struct {// 類型大小size uintptrptrdata uintptr// 類型的 hash 值hash uint32// 類型的 flag,和反射相關(guān)tflag tflag// 內(nèi)存對(duì)齊相關(guān)align uint8fieldalign uint8// 類型的編號(hào),有bool, slice, struct 等等等等kind uint8alg *typeAlg// gc 相關(guān)gcdata *bytestr nameOffptrToThis typeOff }Go 語言各種數(shù)據(jù)類型都是在?_type?字段的基礎(chǔ)上,增加一些額外的字段來進(jìn)行管理的:
type arraytype struct {typ _typeelem *_typeslice *_typelen uintptr }type chantype struct {typ _typeelem *_typedir uintptr }type slicetype struct {typ _typeelem *_type }type structtype struct {typ _typepkgPath namefields []structfield }這些數(shù)據(jù)類型的結(jié)構(gòu)體定義,是反射實(shí)現(xiàn)的基礎(chǔ)。
4. 接口的動(dòng)態(tài)類型和動(dòng)態(tài)值
從源碼里可以看到:iface包含兩個(gè)字段:tab?是接口表指針,指向類型信息;data?是數(shù)據(jù)指針,則指向具體的數(shù)據(jù)。它們分別被稱為動(dòng)態(tài)類型和動(dòng)態(tài)值。而接口值包括動(dòng)態(tài)類型和動(dòng)態(tài)值。
【引申1】接口類型和?nil?作比較
接口值的零值是指動(dòng)態(tài)類型和動(dòng)態(tài)值都為?nil。當(dāng)僅且當(dāng)這兩部分的值都為?nil?的情況下,這個(gè)接口值就才會(huì)被認(rèn)為?接口值 == nil。
來看個(gè)例子:
package mainimport "fmt"type Coder interface {code() }type Gopher struct {name string }func (g Gopher) code() {fmt.Printf("%s is coding\n", g.name) }func main() {var c Coderfmt.Println(c == nil)fmt.Printf("c: %T, %v\n", c, c)var g *Gopherfmt.Println(g == nil)c = gfmt.Println(c == nil)fmt.Printf("c: %T, %v\n", c, c) }輸出:
true c: <nil>, <nil> true false c: *main.Gopher, <nil>一開始,c?的 動(dòng)態(tài)類型和動(dòng)態(tài)值都為?nil,g?也為?nil,當(dāng)把?g?賦值給?c?后,c?的動(dòng)態(tài)類型變成了?*main.Gopher,僅管?c?的動(dòng)態(tài)值仍為?nil,但是當(dāng)?c?和?nil?作比較的時(shí)候,結(jié)果就是?false?了。
【引申2】
來看一個(gè)例子,看一下它的輸出:
函數(shù)運(yùn)行結(jié)果:
<nil> false這里先定義了一個(gè)?MyError?結(jié)構(gòu)體,實(shí)現(xiàn)了?Error?函數(shù),也就實(shí)現(xiàn)了?error?接口。Process?函數(shù)返回了一個(gè)?error?接口,這塊隱含了類型轉(zhuǎn)換。所以,雖然它的值是?nil,其實(shí)它的類型是?*MyError,最后和?nil?比較的時(shí)候,結(jié)果為?false。
【引申3】如何打印出接口的動(dòng)態(tài)類型和值?
直接看代碼:
package mainimport ("unsafe""fmt" )type iface struct {itab, data uintptr }func main() {var a interface{} = nilvar b interface{} = (*int)(nil)x := 5var c interface{} = (*int)(&x)ia := *(*iface)(unsafe.Pointer(&a))ib := *(*iface)(unsafe.Pointer(&b))ic := *(*iface)(unsafe.Pointer(&c))fmt.Println(ia, ib, ic)fmt.Println(*(*int)(unsafe.Pointer(ic.data))) }代碼里直接定義了一個(gè)?iface?結(jié)構(gòu)體,用兩個(gè)指針來描述?itab?和?data,之后將 a, b, c 在內(nèi)存中的內(nèi)容強(qiáng)制解釋成我們自定義的?iface。最后就可以打印出動(dòng)態(tài)類型和動(dòng)態(tài)值的地址。
運(yùn)行結(jié)果如下:
{0 0} {17426912 0} {17426912 842350714568} 5a 的動(dòng)態(tài)類型和動(dòng)態(tài)值的地址均為 0,也就是 nil;b 的動(dòng)態(tài)類型和 c 的動(dòng)態(tài)類型一致,都是?*int;最后,c 的動(dòng)態(tài)值為 5。
5. 編譯器自動(dòng)檢測類型是否實(shí)現(xiàn)接口
經(jīng)常看到一些開源庫里會(huì)有一些類似下面這種奇怪的用法:
var _ io.Writer = (*myWriter)(nil)這時(shí)候會(huì)有點(diǎn)懵,不知道作者想要干什么,實(shí)際上這就是此問題的答案。編譯器會(huì)由此檢查?*myWriter?類型是否實(shí)現(xiàn)了?io.Writer?接口。
來看一個(gè)例子:
package mainimport "io"type myWriter struct {}/*func (w myWriter) Write(p []byte) (n int, err error) {return }*/func main() {// 檢查 *myWriter 類型是否實(shí)現(xiàn)了 io.Writer 接口var _ io.Writer = (*myWriter)(nil)// 檢查 myWriter 類型是否實(shí)現(xiàn)了 io.Writer 接口var _ io.Writer = myWriter{} }注釋掉為 myWriter 定義的 Write 函數(shù)后,運(yùn)行程序:
src/main.go:14:6: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment:*myWriter does not implement io.Writer (missing Write method) src/main.go:15:6: cannot use myWriter literal (type myWriter) as type io.Writer in assignment:myWriter does not implement io.Writer (missing Write method)報(bào)錯(cuò)信息:*myWriter/myWriter 未實(shí)現(xiàn) io.Writer 接口,也就是未實(shí)現(xiàn) Write 方法。
解除注釋后,運(yùn)行程序不報(bào)錯(cuò)。
實(shí)際上,上述賦值語句會(huì)發(fā)生隱式地類型轉(zhuǎn)換,在轉(zhuǎn)換的過程中,編譯器會(huì)檢測等號(hào)右邊的類型是否實(shí)現(xiàn)了等號(hào)左邊接口所規(guī)定的函數(shù)。
總結(jié)一下,可通過在代碼中添加類似如下的代碼,用來檢測類型是否實(shí)現(xiàn)了接口:
var _ io.Writer = (*myWriter)(nil) var _ io.Writer = myWriter{}6. 接口的構(gòu)造過程是怎樣的
我們已經(jīng)看過了?iface?和?eface?的源碼,知道?iface?最重要的是?itab?和?_type。
為了研究清楚接口是如何構(gòu)造的,接下來我會(huì)拿起匯編的武器,還原背后的真相。
來看一個(gè)示例代碼:
package mainimport "fmt"type Person interface {growUp() }type Student struct {age int }func (p Student) growUp() {p.age += 1return }func main() {var qcrao = Person(Student{age: 18})fmt.Println(qcrao) }執(zhí)行命令:
go tool compile -S main.go得到 main 函數(shù)的匯編代碼如下:
0x0000 00000 (./src/main.go:30) TEXT "".main(SB), $80-0 0x0000 00000 (./src/main.go:30) MOVQ (TLS), CX 0x0009 00009 (./src/main.go:30) CMPQ SP, 16(CX) 0x000d 00013 (./src/main.go:30) JLS 157 0x0013 00019 (./src/main.go:30) SUBQ $80, SP 0x0017 00023 (./src/main.go:30) MOVQ BP, 72(SP) 0x001c 00028 (./src/main.go:30) LEAQ 72(SP), BP 0x0021 00033 (./src/main.go:30) FUNCDATA$0, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x0021 00033 (./src/main.go:30) FUNCDATA$1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB) 0x0021 00033 (./src/main.go:31) MOVQ $18, ""..autotmp_1+48(SP) 0x002a 00042 (./src/main.go:31) LEAQ go.itab."".Student,"".Person(SB), AX 0x0031 00049 (./src/main.go:31) MOVQ AX, (SP) 0x0035 00053 (./src/main.go:31) LEAQ ""..autotmp_1+48(SP), AX 0x003a 00058 (./src/main.go:31) MOVQ AX, 8(SP) 0x003f 00063 (./src/main.go:31) PCDATA $0, $0 0x003f 00063 (./src/main.go:31) CALL runtime.convT2I64(SB) 0x0044 00068 (./src/main.go:31) MOVQ 24(SP), AX 0x0049 00073 (./src/main.go:31) MOVQ 16(SP), CX 0x004e 00078 (./src/main.go:33) TESTQ CX, CX 0x0051 00081 (./src/main.go:33) JEQ 87 0x0053 00083 (./src/main.go:33) MOVQ 8(CX), CX 0x0057 00087 (./src/main.go:33) MOVQ $0, ""..autotmp_2+56(SP) 0x0060 00096 (./src/main.go:33) MOVQ $0, ""..autotmp_2+64(SP) 0x0069 00105 (./src/main.go:33) MOVQ CX, ""..autotmp_2+56(SP) 0x006e 00110 (./src/main.go:33) MOVQ AX, ""..autotmp_2+64(SP) 0x0073 00115 (./src/main.go:33) LEAQ ""..autotmp_2+56(SP), AX 0x0078 00120 (./src/main.go:33) MOVQ AX, (SP) 0x007c 00124 (./src/main.go:33) MOVQ $1, 8(SP) 0x0085 00133 (./src/main.go:33) MOVQ $1, 16(SP) 0x008e 00142 (./src/main.go:33) PCDATA $0, $1 0x008e 00142 (./src/main.go:33) CALL fmt.Println(SB) 0x0093 00147 (./src/main.go:34) MOVQ 72(SP), BP 0x0098 00152 (./src/main.go:34) ADDQ $80, SP 0x009c 00156 (./src/main.go:34) RET 0x009d 00157 (./src/main.go:34) NOP 0x009d 00157 (./src/main.go:30) PCDATA $0, $-1 0x009d 00157 (./src/main.go:30) CALL runtime.morestack_noctxt(SB) 0x00a2 00162 (./src/main.go:30) JMP 0我們從第 10 行開始看,如果不理解前面幾行匯編代碼的話,可以回去看看公眾號(hào)前面兩篇文章,這里我就省略了。
| 10-14 | 構(gòu)造調(diào)用?runtime.convT2I64(SB)?的參數(shù) |
我們來看下這個(gè)函數(shù)的參數(shù)形式:
func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {// …… }convT2I64?會(huì)構(gòu)造出一個(gè)?inteface,也就是我們的?Person?接口。
第一個(gè)參數(shù)的位置是?(SP),這里被賦上了?go.itab."".Student,"".Person(SB)?的地址。
我們從生成的匯編找到:
go.itab."".Student,"".Person SNOPTRDATA dupok size=400x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0010 00 00 00 00 00 00 00 00 da 9f 20 d4 rel 0+8 t=1 type."".Person+0rel 8+8 t=1 type."".Student+0size=40?大小為40字節(jié),回顧一下:
type itab struct {inter *interfacetype // 8字節(jié)_type *_type // 8字節(jié)link *itab // 8字節(jié)hash uint32 // 4字節(jié)bad bool // 1字節(jié)inhash bool // 1字節(jié)unused [2]byte // 2字節(jié)fun [1]uintptr // variable sized // 8字節(jié) }把每個(gè)字段的大小相加,itab?結(jié)構(gòu)體的大小就是 40 字節(jié)。上面那一串?dāng)?shù)字實(shí)際上是?itab?序列化后的內(nèi)容,注意到大部分?jǐn)?shù)字是 0,從 24 字節(jié)開始的 4 個(gè)字節(jié)?da 9f 20 d4?實(shí)際上是?itab?的?hash?值,這在判斷兩個(gè)類型是否相同的時(shí)候會(huì)用到。
下面兩行是鏈接指令,簡單說就是將所有源文件綜合起來,給每個(gè)符號(hào)賦予一個(gè)全局的位置值。這里的意思也比較明確:前8個(gè)字節(jié)最終存儲(chǔ)的是?type."".Person?的地址,對(duì)應(yīng)?itab?里的?inter?字段,表示接口類型;8-16 字節(jié)最終存儲(chǔ)的是?type."".Student?的地址,對(duì)應(yīng)?itab?里?_type?字段,表示具體類型。
第二個(gè)參數(shù)就比較簡單了,它就是數(shù)字?18?的地址,這也是初始化?Student?結(jié)構(gòu)體的時(shí)候會(huì)用到。
| 15 | 調(diào)用?runtime.convT2I64(SB) |
具體看下代碼:
func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {t := tab._type//...var x unsafe.Pointerif *(*uint64)(elem) == 0 {x = unsafe.Pointer(&zeroVal[0])} else {x = mallocgc(8, t, false)*(*uint64)(x) = *(*uint64)(elem)}i.tab = tabi.data = xreturn }這塊代碼比較簡單,把?tab?賦給了?iface?的?tab?字段;data?部分則是在堆上申請(qǐng)了一塊內(nèi)存,然后將?elem?指向的?18?拷貝過去。這樣?iface?就組裝好了。
| 17 | 把?i.tab?賦給?CX |
| 18 | 把?i.data?賦給?AX |
| 19-21 | 檢測?i.tab?是否是 nil,如果不是的話,把 CX 移動(dòng) 8 個(gè)字節(jié),也就是把?itab?的?_type?字段賦給了 CX,這也是接口的實(shí)體類型,最終要作為?fmt.Println?函數(shù)的參數(shù) |
后面,就是調(diào)用?fmt.Println?函數(shù)及之前的參數(shù)準(zhǔn)備工作了,不再贅述。
這樣,我們就把一個(gè)?interface?的構(gòu)造過程說完了。
【引申1】
如何打印出接口類型的?Hash?值?
這里參考曹大神翻譯的一篇文章,參考資料里會(huì)寫上。具體做法如下:
type iface struct {tab *itabdata unsafe.Pointer } type itab struct {inter uintptr_type uintptrlink uintptrhash uint32_ [4]bytefun [1]uintptr }func main() {var qcrao = Person(Student{age: 18})iface := (*iface)(unsafe.Pointer(&qcrao))fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash) }定義了一個(gè)山寨版的?iface?和?itab,說它山寨是因?yàn)?itab?里的一些關(guān)鍵數(shù)據(jù)結(jié)構(gòu)都不具體展開了,比如?_type,對(duì)比一下正宗的定義就可以發(fā)現(xiàn),但是山寨版依然能工作,因?yàn)?_type?就是一個(gè)指針而已嘛。
在?main?函數(shù)里,先構(gòu)造出一個(gè)接口對(duì)象?qcrao,然后強(qiáng)制類型轉(zhuǎn)換,最后讀取出?hash?值,非常妙!你也可以自己動(dòng)手試一下。
運(yùn)行結(jié)果:
iface.tab.hash = 0xd4209fda值得一提的是,構(gòu)造接口?qcrao?的時(shí)候,即使我把?age?寫成其他值,得到的?hash?值依然不變的,這應(yīng)該是可以預(yù)料的,hash?值只和他的字段、方法相關(guān)。
7. 類型轉(zhuǎn)換和斷言的區(qū)別
我們知道,Go 語言中不允許隱式類型轉(zhuǎn)換,也就是說?=?兩邊,不允許出現(xiàn)類型不相同的變量。
類型轉(zhuǎn)換、類型斷言本質(zhì)都是把一個(gè)類型轉(zhuǎn)換成另外一個(gè)類型。不同之處在于,類型斷言是對(duì)接口變量進(jìn)行的操作。
類型轉(zhuǎn)換
對(duì)于類型轉(zhuǎn)換而言,轉(zhuǎn)換前后的兩個(gè)類型要相互兼容才行。類型轉(zhuǎn)換的語法為:
<結(jié)果類型> := <目標(biāo)類型> ( <表達(dá)式> )
package mainimport "fmt"func main() {var i int = 9var f float64f = float64(i)fmt.Printf("%T, %v\n", f, f)f = 10.8a := int(f)fmt.Printf("%T, %v\n", a, a)// s := []int(i) }上面的代碼里,我定義了一個(gè)?int?型和?float64?型的變量,嘗試在它們之前相互轉(zhuǎn)換,結(jié)果是成功的:int?型和?float64?是相互兼容的。
如果我把最后一行代碼的注釋去掉,編譯器會(huì)報(bào)告類型不兼容的錯(cuò)誤:
cannot convert i (type int) to type []int斷言
前面說過,因?yàn)榭战涌?interface{}?沒有定義任何函數(shù),因此 Go 中所有類型都實(shí)現(xiàn)了空接口。當(dāng)一個(gè)函數(shù)的形參是?interface{},那么在函數(shù)中,需要對(duì)形參進(jìn)行斷言,從而得到它的真實(shí)類型。
斷言的語法為:
<目標(biāo)類型的值>,<布爾參數(shù)> := <表達(dá)式>.( 目標(biāo)類型 ) // 安全類型斷言
<目標(biāo)類型的值> := <表達(dá)式>.( 目標(biāo)類型 ) //非安全類型斷言
類型轉(zhuǎn)換和類型斷言有些相似,不同之處,在于類型斷言是對(duì)接口進(jìn)行的操作。
還是來看一個(gè)簡短的例子:
package mainimport "fmt"type Student struct {Name stringAge int }func main() {var i interface{} = new(Student)s := i.(Student)fmt.Println(s) }運(yùn)行一下:
panic: interface conversion: interface {} is *main.Student, not main.Student直接?panic?了,這是因?yàn)?i?是?*Student?類型,并非?Student?類型,斷言失敗。這里直接發(fā)生了?panic,線上代碼可能并不適合這樣做,可以采用“安全斷言”的語法:
func main() {var i interface{} = new(Student)s, ok := i.(Student)if ok {fmt.Println(s)} }這樣,即使斷言失敗也不會(huì)?panic。
斷言其實(shí)還有另一種形式,就是用在利用?switch?語句判斷接口的類型。每一個(gè)?case?會(huì)被順序地考慮。當(dāng)命中一個(gè)?case?時(shí),就會(huì)執(zhí)行?case?中的語句,因此?case?語句的順序是很重要的,因?yàn)楹苡锌赡軙?huì)有多個(gè)?case?匹配的情況。
代碼示例如下:
func main() {//var i interface{} = new(Student)//var i interface{} = (*Student)(nil)var i interface{}fmt.Printf("%p %v\n", &i, i)judge(i) }func judge(v interface{}) {fmt.Printf("%p %v\n", &v, v)switch v := v.(type) {case nil:fmt.Printf("%p %v\n", &v, v)fmt.Printf("nil type[%T] %v\n", v, v)case Student:fmt.Printf("%p %v\n", &v, v)fmt.Printf("Student type[%T] %v\n", v, v)case *Student:fmt.Printf("%p %v\n", &v, v)fmt.Printf("*Student type[%T] %v\n", v, v)default:fmt.Printf("%p %v\n", &v, v)fmt.Printf("unknow\n")} }type Student struct {Name stringAge int }main?函數(shù)里有三行不同的聲明,每次運(yùn)行一行,注釋另外兩行,得到三組運(yùn)行結(jié)果:
// --- var i interface{} = new(Student) 0xc4200701b0 [Name: ], [Age: 0] 0xc4200701d0 [Name: ], [Age: 0] 0xc420080020 [Name: ], [Age: 0] *Student type[*main.Student] [Name: ], [Age: 0]// --- var i interface{} = (*Student)(nil) 0xc42000e1d0 <nil> 0xc42000e1f0 <nil> 0xc42000c030 <nil> *Student type[*main.Student] <nil>// --- var i interface{} 0xc42000e1d0 <nil> 0xc42000e1e0 <nil> 0xc42000e1f0 <nil> nil type[<nil>] <nil>對(duì)于第一行語句:
var i interface{} = new(Student)i?是一個(gè)?*Student?類型,匹配上第三個(gè) case,從打印的三個(gè)地址來看,這三處的變量實(shí)際上都是不一樣的。在?main?函數(shù)里有一個(gè)局部變量?i;調(diào)用函數(shù)時(shí),實(shí)際上是復(fù)制了一份參數(shù),因此函數(shù)里又有一個(gè)變量?v,它是?i?的拷貝;斷言之后,又生成了一份新的拷貝。所以最終打印的三個(gè)變量的地址都不一樣。
對(duì)于第二行語句:
var i interface{} = (*Student)(nil)這里想說明的其實(shí)是?i?在這里動(dòng)態(tài)類型是?(*Student), 數(shù)據(jù)為?nil,它的類型并不是?nil,它與?nil?作比較的時(shí)候,得到的結(jié)果也是?false。
最后一行語句:
var i interface{}這回?i?才是?nil?類型。
【引申1】
fmt.Println?函數(shù)的參數(shù)是?interface。對(duì)于內(nèi)置類型,函數(shù)內(nèi)部會(huì)用窮舉法,得出它的真實(shí)類型,然后轉(zhuǎn)換為字符串打印。而對(duì)于自定義類型,首先確定該類型是否實(shí)現(xiàn)了?String()?方法,如果實(shí)現(xiàn)了,則直接打印輸出?String()?方法的結(jié)果;否則,會(huì)通過反射來遍歷對(duì)象的成員進(jìn)行打印。
再來看一個(gè)簡短的例子,比較簡單,不要緊張:
package mainimport "fmt"type Student struct {Name stringAge int }func main() {var s = Student{Name: "qcrao",Age: 18,}fmt.Println(s) }因?yàn)?Student?結(jié)構(gòu)體沒有實(shí)現(xiàn)?String()?方法,所以?fmt.Println?會(huì)利用反射挨個(gè)打印成員變量:
{qcrao 18}增加一個(gè)?String()?方法的實(shí)現(xiàn):
func (s Student) String() string {return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age) }打印結(jié)果:
[Name: qcrao], [Age: 18]按照我們自定義的方法來打印了。
【引申2】
針對(duì)上面的例子,如果改一下:
注意看兩個(gè)函數(shù)的接受者類型不同,現(xiàn)在?Student?結(jié)構(gòu)體只有一個(gè)接受者類型為?指針類型?的?String()?函數(shù),打印結(jié)果:
{qcrao 18}為什么?
類型?T?只有接受者是?T?的方法;而類型?*T?擁有接受者是?T?和?*T?的方法。語法上?T?能直接調(diào)?*T?的方法僅僅是?Go?的語法糖。
所以,?Student?結(jié)構(gòu)體定義了接受者類型是值類型的?String()?方法時(shí),通過
fmt.Println(s) fmt.Println(&s)均可以按照自定義的格式來打印。
如果?Student?結(jié)構(gòu)體定義了接受者類型是指針類型的?String()?方法時(shí),只有通過
fmt.Println(&s)才能按照自定義的格式打印。
8. 接口轉(zhuǎn)換的原理
通過前面提到的?iface?的源碼可以看到,實(shí)際上它包含接口的類型?interfacetype?和 實(shí)體類型的類型?_type,這兩者都是?iface?的字段?itab?的成員。也就是說生成一個(gè)?itab?同時(shí)需要接口的類型和實(shí)體的類型。
<interface 類型, 實(shí)體類型> ->itable
當(dāng)判定一種類型是否滿足某個(gè)接口時(shí),Go 使用類型的方法集和接口所需要的方法集進(jìn)行匹配,如果類型的方法集完全包含接口的方法集,則可認(rèn)為該類型實(shí)現(xiàn)了該接口。
例如某類型有?m?個(gè)方法,某接口有?n?個(gè)方法,則很容易知道這種判定的時(shí)間復(fù)雜度為?O(mn),Go 會(huì)對(duì)方法集的函數(shù)按照函數(shù)名的字典序進(jìn)行排序,所以實(shí)際的時(shí)間復(fù)雜度為?O(m+n)。
這里我們來探索將一個(gè)接口轉(zhuǎn)換給另外一個(gè)接口背后的原理,當(dāng)然,能轉(zhuǎn)換的原因必然是類型兼容。
直接來看一個(gè)例子:
package mainimport "fmt"type coder interface {code()run() }type runner interface {run() }type Gopher struct {language string }func (g Gopher) code() {return }func (g Gopher) run() {return }func main() {var c coder = Gopher{}var r runnerr = cfmt.Println(c, r) }簡單解釋下上述代碼:定義了兩個(gè)?interface:?coder?和?runner。定義了一個(gè)實(shí)體類型?Gopher,類型?Gopher?實(shí)現(xiàn)了兩個(gè)方法,分別是?run()?和?code()。main 函數(shù)里定義了一個(gè)接口變量?c,綁定了一個(gè)?Gopher?對(duì)象,之后將?c?賦值給另外一個(gè)接口變量?r?。賦值成功的原因是?c?中包含?run()?方法。這樣,兩個(gè)接口變量完成了轉(zhuǎn)換。
執(zhí)行命令:
go tool compile -S ./src/main.go得到 main 函數(shù)的匯編命令,可以看到:?r = c?這一行語句實(shí)際上是調(diào)用了?runtime.convI2I(SB),也就是?convI2I?函數(shù),從函數(shù)名來看,就是將一個(gè)?interface?轉(zhuǎn)換成另外一個(gè)?interface,看下它的源代碼:
func convI2I(inter *interfacetype, i iface) (r iface) {tab := i.tabif tab == nil {return}if tab.inter == inter {r.tab = tabr.data = i.datareturn}r.tab = getitab(inter, tab._type, false)r.data = i.datareturn }代碼比較簡單,函數(shù)參數(shù)?inter?表示接口類型,i?表示綁定了實(shí)體類型的接口,r?則表示接口轉(zhuǎn)換了之后的新的?iface。通過前面的分析,我們又知道,?iface?是由?tab?和?data?兩個(gè)字段組成。所以,實(shí)際上?convI2I?函數(shù)真正要做的事,找到新?interface?的?tab?和?data,就大功告成了。
我們還知道,tab?是由接口類型?interfacetype?和 實(shí)體類型?_type?組成。所以最關(guān)鍵的語句是?r.tab = getitab(inter, tab._type, false)。
因此,重點(diǎn)來看下?getitab?函數(shù)的源碼,只看關(guān)鍵的地方:
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {// ……// 根據(jù) inter, typ 計(jì)算出 hash 值h := itabhash(inter, typ)// look twice - once without lock, once with.// common case will be no lock contention.var m *itabvar locked intfor locked = 0; locked < 2; locked++ {if locked != 0 {lock(&ifaceLock)}// 遍歷哈希表的一個(gè) slotfor m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {// 如果在 hash 表中已經(jīng)找到了 itab(inter 和 typ 指針都相同)if m.inter == inter && m._type == typ {// ……if locked != 0 {unlock(&ifaceLock)}return m}}}// 在 hash 表中沒有找到 itab,那么新生成一個(gè) itabm = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))m.inter = interm._type = typ// 添加到全局的 hash 表中additab(m, true, canfail)unlock(&ifaceLock)if m.bad {return nil}return m }簡單總結(jié)一下:getitab 函數(shù)會(huì)根據(jù)?interfacetype?和?_type?去全局的 itab 哈希表中查找,如果能找到,則直接返回;否則,會(huì)根據(jù)給定的?interfacetype?和?_type?新生成一個(gè)?itab,并插入到 itab 哈希表,這樣下一次就可以直接拿到?itab。
這里查找了兩次,并且第二次上鎖了,這是因?yàn)槿绻谝淮螞]找到,在第二次仍然沒有找到相應(yīng)的?itab?的情況下,需要新生成一個(gè),并且寫入哈希表,因此需要加鎖。這樣,其他協(xié)程在查找相同的?itab?并且也沒有找到時(shí),第二次查找時(shí),會(huì)被掛住,之后,就會(huì)查到第一個(gè)協(xié)程寫入哈希表的?itab。
再來看一下?additab?函數(shù)的代碼:
// 檢查 _type 是否符合 interface_type 并且創(chuàng)建對(duì)應(yīng)的 itab 結(jié)構(gòu)體 將其放到 hash 表中 func additab(m *itab, locked, canfail bool) {inter := m.intertyp := m._typex := typ.uncommon()// both inter and typ have method sorted by name,// and interface names are unique,// so can iterate over both in lock step;// the loop is O(ni+nt) not O(ni*nt).// // inter 和 typ 的方法都按方法名稱進(jìn)行了排序// 并且方法名都是唯一的。所以循環(huán)的次數(shù)是固定的// 只用循環(huán) O(ni+nt),而非 O(ni*nt)ni := len(inter.mhdr)nt := int(x.mcount)xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]j := 0for k := 0; k < ni; k++ {i := &inter.mhdr[k]itype := inter.typ.typeOff(i.ityp)name := inter.typ.nameOff(i.name)iname := name.name()ipkg := name.pkgPath()if ipkg == "" {ipkg = inter.pkgpath.name()}for ; j < nt; j++ {t := &xmhdr[j]tname := typ.nameOff(t.name)// 檢查方法名字是否一致if typ.typeOff(t.mtyp) == itype && tname.name() == iname {pkgPath := tname.pkgPath()if pkgPath == "" {pkgPath = typ.nameOff(x.pkgpath).name()}if tname.isExported() || pkgPath == ipkg {if m != nil {// 獲取函數(shù)地址,并加入到itab.fun數(shù)組中ifn := typ.textOff(t.ifn)*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn}goto nextimethod}}}// ……m.bad = truebreaknextimethod:}if !locked {throw("invalid itab locking")}// 計(jì)算 hash 值h := itabhash(inter, typ)// 加到Hash Slot鏈表中m.link = hash[h]m.inhash = trueatomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m)) }additab?會(huì)檢查?itab?持有的?interfacetype?和?_type?是否符合,就是看?_type?是否完全實(shí)現(xiàn)了?interfacetype?的方法,也就是看兩者的方法列表重疊的部分就是?interfacetype?所持有的方法列表。注意到其中有一個(gè)雙層循環(huán),乍一看,循環(huán)次數(shù)是?ni * nt,但由于兩者的函數(shù)列表都按照函數(shù)名稱進(jìn)行了排序,因此最終只執(zhí)行了?ni + nt?次,代碼里通過一個(gè)小技巧來實(shí)現(xiàn):第二層循環(huán)并沒有從 0 開始計(jì)數(shù),而是從上一次遍歷到的位置開始。
求 hash 值的函數(shù)比較簡單:
func itabhash(inter *interfacetype, typ *_type) uint32 {h := inter.typ.hashh += 17 * typ.hashreturn h % hashSize }hashSize?的值是 1009。
更一般的,當(dāng)把實(shí)體類型賦值給接口的時(shí)候,會(huì)調(diào)用?conv?系列函數(shù),例如空接口調(diào)用?convT2E?系列、非空接口調(diào)用?convT2I?系列。這些函數(shù)比較相似:
9. 如何用 interface 實(shí)現(xiàn)多態(tài)
Go?語言并沒有設(shè)計(jì)諸如虛函數(shù)、純虛函數(shù)、繼承、多重繼承等概念,但它通過接口卻非常優(yōu)雅地支持了面向?qū)ο蟮奶匦浴?/p>
多態(tài)是一種運(yùn)行期的行為,它有以下幾個(gè)特點(diǎn):
看一個(gè)實(shí)現(xiàn)了多態(tài)的代碼例子:
package mainimport "fmt"func main() {qcrao := Student{age: 18}whatJob(&qcrao)growUp(&qcrao)fmt.Println(qcrao)stefno := Programmer{age: 100}whatJob(stefno)growUp(stefno)fmt.Println(stefno) }func whatJob(p Person) {p.job() }func growUp(p Person) {p.growUp() }type Person interface {job()growUp() }type Student struct {age int }func (p Student) job() {fmt.Println("I am a student.")return }func (p *Student) growUp() {p.age += 1return }type Programmer struct {age int }func (p Programmer) job() {fmt.Println("I am a programmer.")return }func (p Programmer) growUp() {// 程序員老得太快 ^_^p.age += 10return }代碼里先定義了 1 個(gè)?Person?接口,包含兩個(gè)函數(shù):
job() growUp()然后,又定義了 2 個(gè)結(jié)構(gòu)體,Student?和?Programmer,同時(shí),類型?*Student、Programmer?實(shí)現(xiàn)了?Person?接口定義的兩個(gè)函數(shù)。注意,*Student?類型實(shí)現(xiàn)了接口,?Student?類型卻沒有。
之后,我又定義了函數(shù)參數(shù)是?Person?接口的兩個(gè)函數(shù):
func whatJob(p Person) func growUp(p Person)main?函數(shù)里先生成?Student?和?Programmer?的對(duì)象,再將它們分別傳入到函數(shù)?whatJob?和?growUp。函數(shù)中,直接調(diào)用接口函數(shù),實(shí)際執(zhí)行的時(shí)候是看最終傳入的實(shí)體類型是什么,調(diào)用的是實(shí)體類型實(shí)現(xiàn)的函數(shù)。于是,不同對(duì)象針對(duì)同一消息就有多種表現(xiàn),多態(tài)就實(shí)現(xiàn)了。
更深入一點(diǎn)來說的話,在函數(shù)?whatJob()?或者?growUp()?內(nèi)部,接口?person?綁定了實(shí)體類型?*Student?或者?Programmer。根據(jù)前面分析的?iface?源碼,這里會(huì)直接調(diào)用?fun?里保存的函數(shù),類似于:?s.tab->fun[0],而因?yàn)?fun?數(shù)組里保存的是實(shí)體類型實(shí)現(xiàn)的函數(shù),所以當(dāng)函數(shù)傳入不同的實(shí)體類型時(shí),調(diào)用的實(shí)際上是不同的函數(shù)實(shí)現(xiàn),從而實(shí)現(xiàn)多態(tài)。
運(yùn)行一下代碼:
I am a student. {19} I am a programmer. {100}10. Go 接口與 C++ 接口有何異同
接口定義了一種規(guī)范,描述了類的行為和功能,而不做具體實(shí)現(xiàn)。
C++ 的接口是使用抽象類來實(shí)現(xiàn)的,如果類中至少有一個(gè)函數(shù)被聲明為純虛函數(shù),則這個(gè)類就是抽象類。純虛函數(shù)是通過在聲明中使用 “= 0” 來指定的。例如:
class Shape {public:// 純虛函數(shù)virtual double getArea() = 0;private:string name; // 名稱 };設(shè)計(jì)抽象類的目的,是為了給其他類提供一個(gè)可以繼承的適當(dāng)?shù)幕悺3橄箢惒荒鼙挥糜趯?shí)例化對(duì)象,它只能作為接口使用。
派生類需要明確地聲明它繼承自基類,并且需要實(shí)現(xiàn)基類中所有的純虛函數(shù)。
C++ 定義接口的方式稱為“侵入式”,而 Go 采用的是 “非侵入式”,不需要顯式聲明,只需要實(shí)現(xiàn)接口定義的函數(shù),編譯器自動(dòng)會(huì)識(shí)別。
C++ 和 Go 在定義接口方式上的不同,也導(dǎo)致了底層實(shí)現(xiàn)上的不同。C++ 通過虛函數(shù)表來實(shí)現(xiàn)基類調(diào)用派生類的函數(shù);而 Go 通過?itab?中的?fun?字段來實(shí)現(xiàn)接口變量調(diào)用實(shí)體類型的函數(shù)。C++ 中的虛函數(shù)表是在編譯期生成的;而 Go 的?itab?中的?fun?字段是在運(yùn)行期間動(dòng)態(tài)生成的。原因在于,Go 中實(shí)體類型可能會(huì)無意中實(shí)現(xiàn) N 多接口,很多接口并不是本來需要的,所以不能為類型實(shí)現(xiàn)的所有接口都生成一個(gè)?itab, 這也是“非侵入式”帶來的影響;這在 C++ 中是不存在的,因?yàn)榕缮枰@示聲明它繼承自哪個(gè)基類。
總結(jié)
以上是生活随笔為你收集整理的golang 接口类型 interface 简介使用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [SCOI2008] 奖励关
- 下一篇: CC攻击(N个免费代理形成的DDOS)