<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          golang 拾遺:指針和接口

          共 8837字,需瀏覽 18分鐘

           ·

          2020-10-21 13:21

          點(diǎn)擊上方藍(lán)色“Go語言中文網(wǎng)”關(guān)注,每天一起學(xué) Go

          作者:@apocelipes[1]本文為作者原創(chuàng),轉(zhuǎn)載請(qǐng)注明出處:https://www.cnblogs.com/apocelipes/p/13796041.html


          這是本系列的第一篇文章,golang 拾遺主要是用來記錄一些遺忘了的、平時(shí)從沒注意過的 golang 相關(guān)知識(shí)。想做本系列的契機(jī)其實(shí)是因?yàn)橐咔殚e著在家無聊,網(wǎng)上沖浪的時(shí)候發(fā)現(xiàn)了 zhuihu 上的go 語言愛好者周刊[2]Go 101[3],讀之如醍醐灌頂,受益匪淺,于是本系列的文章就誕生了。拾遺主要是收集和 golang 相關(guān)的瑣碎知識(shí),當(dāng)然也會(huì)對(duì)周刊和 101 的內(nèi)容做一些補(bǔ)充說明。好了,題外話就此打住,下面該進(jìn)入今天的正題了。

          指針和接口

          golang 的類型系統(tǒng)其實(shí)很有意思,有意思的地方就在于類型系統(tǒng)表面上看起來眾生平等,然而實(shí)際上卻要分成普通類型(types)和接口(interfaces)來看待。普通類型也包含了所謂的引用類型,例如slicemap,雖然他們和interface同為引用類型,但是行為更趨近于普通的內(nèi)置類型和自定義類型,因此只有特立獨(dú)行的interface會(huì)被單獨(dú)歸類。

          那我們是依據(jù)什么把 golang 的類型分成兩類的呢?其實(shí)很簡(jiǎn)單,看類型能不能在編譯期就確定以及調(diào)用的類型方法是否能在編譯期被確定。

          如果覺得上面的解釋太過抽象的可以先看一下下面的例子:

          package?main

          import?"fmt"

          func?main(){
          ????m?:=?make(map[int]int)
          ????m[1]?=?1?*?2
          ????m[2]?=?2?*?2
          ????fmt.Println(m)
          ????m2?:=?make(map[string]int)
          ????m2["python"]?=?1
          ????m2["golang"]?=?2
          ????fmt.Println(m2)
          }

          首先我們來看非 interface 的引用類型,mm2明顯是兩個(gè)不同的類型,不過實(shí)際上在底層他們是一樣的,不信我們用 objdump 工具檢查一下:

          go tool objdump -s 'main\.main' a

          TEXT main.main(SB) /tmp/a.go
          a.go:6 CALL runtime.makemap_small(SB) # m := make(map[int]int)
          ...
          a.go:7 CALL runtime.mapassign_fast64(SB) # m[1] = 1 * 2
          ...
          a.go:8 CALL runtime.mapassign_fast64(SB) # m[2] = 2 * 2
          ...
          ...
          a.go:10 CALL runtime.makemap_small(SB) # m2 := make(map[string]int)
          ...
          a.go:11 CALL runtime.mapassign_faststr(SB) # m2["python"] = 1
          ...
          a.go:12 CALL runtime.mapassign_faststr(SB) # m2["golang"] = 2

          省略了一些寄存器的操作和無關(guān)函數(shù)的調(diào)用,順便加上了對(duì)應(yīng)的代碼的原文,我們可以清晰地看到盡管類型不同,但 map 調(diào)用的方法都是相同的而且是編譯期就已經(jīng)確定的。如果是自定義類型呢?

          package?main

          import?"fmt"

          type?Person?struct?{
          ????name?string
          ????age?int
          }

          func?(p?*Person)?sayHello()?{
          ????fmt.Printf("Hello,?I'm?%v,?%v?year(s)?old\n",?p.name,?p.age)
          }

          func?main(){
          ????p?:=?Person{
          ????????name:?"apocelipes",
          ????????age:?100,
          ????}
          ????p.sayHello()
          }

          這次我們創(chuàng)建了一個(gè)擁有自定義字段和方法的自定義類型,下面再用 objdump 檢查一下:

          go tool objdump -s 'main\.main' b

          TEXT main.main(SB) /tmp/b.go
          ...
          b.go:19 CALL main.(*Person).sayHello(SB)
          ...

          用字面量創(chuàng)建對(duì)象和初始化調(diào)用堆棧的匯編代碼不是重點(diǎn),重點(diǎn)在于那句CALL,我們可以看到自定義類型的方法也是在編譯期就確定了的。

          那反過來看看 interface 會(huì)有什么區(qū)別:

          package?main

          import?"fmt"

          type?Worker?interface?{
          ????Work()
          }

          type?Typist?struct{}
          func?(*Typist)Work()?{
          ????fmt.Println("Typing...")
          }

          type?Programer?struct{}
          func?(*Programer)Work()?{
          ????fmt.Println("Programming...")
          }

          func?main(){
          ????var?w?Worker?=?&Typist{}
          ????w.Work()
          ????w?=?&Programer{}
          ????w.Work()
          }

          注意!編譯這個(gè)程序需要禁止編譯器進(jìn)行優(yōu)化,否則編譯器會(huì)把接口的方法查找直接優(yōu)化為特定類型的方法調(diào)用:

          go build -gcflags "-N -l" c.go
          go tool objdump -S -s 'main\.main' c

          TEXT main.main(SB) /tmp/c.go
          ...
          var w Worker = &Typist{}
          LEAQ runtime.zerobase(SB), AX
          MOVQ AX, 0x10(SP)
          MOVQ AX, 0x20(SP)
          LEAQ go.itab.*main.Typist,main.Worker(SB), CX
          MOVQ CX, 0x28(SP)
          MOVQ AX, 0x30(SP)
          w.Work()
          MOVQ 0x28(SP), AX
          TESTB AL, 0(AX)
          MOVQ 0x18(AX), AX
          MOVQ 0x30(SP), CX
          MOVQ CX, 0(SP)
          CALL AX
          w = &Programer{}
          LEAQ runtime.zerobase(SB), AX
          MOVQ AX, 0x8(SP)
          MOVQ AX, 0x18(SP)
          LEAQ go.itab.*main.Programer,main.Worker(SB), CX
          MOVQ CX, 0x28(SP)
          MOVQ AX, 0x30(SP)
          w.Work()
          MOVQ 0x28(SP), AX
          TESTB AL, 0(AX)
          MOVQ 0x18(AX), AX
          MOVQ 0x30(SP), CX
          MOVQ CX, 0(SP)
          CALL AX
          ...

          這次我們可以看到調(diào)用接口的方法會(huì)去在 runtime 進(jìn)行查找,隨后CALL找到的地址,而不是像之前那樣在編譯期就能找到對(duì)應(yīng)的函數(shù)直接調(diào)用。這就是 interface 為什么特殊的原因:interface 是動(dòng)態(tài)變化的類型。

          可以動(dòng)態(tài)變化的類型最顯而易見的好處是給予程序高度的靈活性,但靈活性是要付出代價(jià)的,主要在兩方面。

          一是性能代價(jià)。動(dòng)態(tài)的方法查找總是要比編譯期就能確定的方法調(diào)用多花費(fèi)幾條匯編指令(mov 和 lea 通常都是會(huì)產(chǎn)生實(shí)際指令的),數(shù)量累計(jì)后就會(huì)產(chǎn)生性能影響。不過好消息是通常編譯器對(duì)我們的代碼進(jìn)行了優(yōu)化,例如c.go中如果我們不關(guān)閉編譯器的優(yōu)化,那么編譯器會(huì)在編譯期間就替我們完成方法的查找,實(shí)際生產(chǎn)的代碼里不會(huì)有動(dòng)態(tài)查找的內(nèi)容。然而壞消息是這種優(yōu)化需要編譯器可以在編譯期確定接口引用數(shù)據(jù)的實(shí)際類型,考慮如下代碼:

          type?Worker?interface?{
          ????Work()
          }

          for?_,?v?:=?workers?{
          ????v.Work()
          }

          因?yàn)橹灰獙?shí)現(xiàn)了Worker接口的類型就可以把自己的實(shí)例塞進(jìn)workers切片里,所以編譯器不能確定 v 引用的數(shù)據(jù)的類型,優(yōu)化自然也無從談起了。

          而另一個(gè)代價(jià),確切地說其實(shí)應(yīng)該叫陷阱,就是接下來我們要探討的主題了。

          golang 的指針

          指針也是一個(gè)極有探討價(jià)值的話題,特別是指針在 reflect 以及 runtime 包里的各種黑科技。不過放輕松,今天我們只用了解下指針的自動(dòng)解引用。

          我們把b.go里的代碼改動(dòng)一行:

          p?:=?&Person{
          ????name:?"apocelipes",
          ????age:?100,
          }

          p 現(xiàn)在是個(gè)指針,其余代碼不需要任何改動(dòng),程序依舊可以正常編譯執(zhí)行。對(duì)應(yīng)的匯編是這樣的畫風(fēng)(當(dāng)然得關(guān)閉優(yōu)化):

          p.sayHello()
          MOVQ AX, 0(SP)
          CALL main.(*Person).sayHello(SB)

          對(duì)比一下非指針版本:

          p.sayHello()
          LEAQ 0x8(SP), AX
          MOVQ AX, 0(SP)
          CALL main.(*Person).sayHello(SB)

          與其說是指針自動(dòng)解引用,倒不如說是非指針版本先求出了對(duì)象的實(shí)際地址,隨后傳入了這個(gè)地址作為方法的接收器調(diào)用了方法。這也沒什么好奇怪的,因?yàn)槲覀兊姆椒ㄊ侵羔樈邮掌鳎篜。

          如果把接收器換成值類型接收器:

          p.sayHello()
          TESTB AL, 0(AX)
          MOVQ 0x40(SP), AX
          MOVQ 0x48(SP), CX
          MOVQ 0x50(SP), DX
          MOVQ AX, 0x28(SP)
          MOVQ CX, 0x30(SP)
          MOVQ DX, 0x38(SP)
          MOVQ AX, 0(SP)
          MOVQ CX, 0x8(SP)
          MOVQ DX, 0x10(SP)
          CALL main.Person.sayHello(SB)

          作為對(duì)比:

          p.sayHello()
          MOVQ AX, 0(SP)
          MOVQ $0xa, 0x8(SP)
          MOVQ $0x64, 0x10(SP)
          CALL main.Person.sayHello(SB)

          這時(shí)候 golang 就是先檢查指針隨后解引用了。同時(shí)要注意,這里的方法調(diào)用是已經(jīng)在編譯期確定了的。

          指向 interface 的指針

          鋪墊了這么久,終于該進(jìn)入正題了。不過在此之前還有一點(diǎn)小小的預(yù)備知識(shí)需要提一下:

          A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer. --- go language spec

          換而言之,只要是能取地址的類型就有對(duì)應(yīng)的指針類型,比較巧的是在 golang 里引用類型是可以取地址的,包括 interface。

          有了這些鋪墊,現(xiàn)在我們可以看一下我們的說唱歌手程序了:

          package?main

          import?"fmt"

          type?Rapper?interface?{
          ????Rap()?string
          }

          type?Dean?struct?{}

          func?(_?Dean)?Rap()?string?{
          ????return?"Im?a?rapper"
          }

          func?doRap(p?*Rapper)?{
          ????fmt.Println(p.Rap())
          }

          func?main(){
          ????i?:=?new(Rapper)
          ????*i?=?Dean{}
          ????fmt.Println(i.Rap())
          ????doRap(i)
          }

          問題來了,小青年 Dean 能圓自己的說唱夢(mèng)么?

          很遺憾,編譯器給出了反對(duì)意見:

          # command-line-arguments
          ./rapper.go:16:18: p.Rap undefined (type *Rapper is pointer to interface, not interface)
          ./rapper.go:22:18: i.Rap undefined (type *Rapper is pointer to interface, not interface)

          也許type *XXX is pointer to interface, not interface這個(gè)錯(cuò)誤你并不陌生,你曾經(jīng)也犯過用指針指向 interface 的錯(cuò)誤,經(jīng)過一番搜索后你找到了一篇教程,或者是博客,有或者是隨便什么地方的資料,他們都會(huì)告訴你不應(yīng)該用指針去指向接口,接口本身是引用類型無需再用指針去引用。

          其實(shí)他們只說對(duì)了一半,事實(shí)上只要把 i 和 p 改成接口類型就可以正常編譯運(yùn)行了。沒說對(duì)的一半是指針可以指向接口,也可以使用接口的方法,但是要繞些彎路(當(dāng)然,用指針引用接口通常是多此一舉,所以聽從經(jīng)驗(yàn)之談也沒什么不好的):

          func?doRap(p?*Rapper)?{
          ????fmt.Println((*p).Rap())
          }

          func?main(){
          ????i?:=?new(Rapper)
          ????*i?=?Dean{}
          ????fmt.Println((*i).Rap())
          ????doRap(i)
          }
          go?run?rapper.go

          Im?a?rapper
          Im?a?rapper

          神奇的一幕出現(xiàn)了,程序不僅沒報(bào)錯(cuò)而且運(yùn)行得很正常。但是這和 golang 對(duì)指針的自動(dòng)解引用有什么區(qū)別呢?明明看起來都一樣但就是第一種方案會(huì)報(bào) 找不到Rap方法?

          為了方便觀察,我們把調(diào)用語句單獨(dú)抽出來,然后查看未優(yōu)化過的匯編碼:

          s := (*p).Rap()
          0x498ee1 488b842488000000 MOVQ 0x88(SP), AX
          0x498ee9 8400 TESTB AL, 0(AX)
          0x498eeb 488b08 MOVQ 0(AX), CX
          0x498eee 8401 TESTB AL, 0(CX)
          0x498ef0 488b4008 MOVQ 0x8(AX), AX
          0x498ef4 488b4918 MOVQ 0x18(CX), CX
          0x498ef8 48890424 MOVQ AX, 0(SP)
          0x498efc ffd1 CALL CX

          拋開手工解引用的部分,后 6 行其實(shí)和直接使用 interface 進(jìn)行動(dòng)態(tài)查詢是一樣的。真正的問題其實(shí)出在自動(dòng)解引用上:

          p.sayHello()
          TESTB AL, 0(AX)
          MOVQ 0x40(SP), AX
          MOVQ 0x48(SP), CX
          MOVQ 0x50(SP), DX
          MOVQ AX, 0x28(SP)
          MOVQ CX, 0x30(SP)
          MOVQ DX, 0x38(SP)
          MOVQ AX, 0(SP)
          MOVQ CX, 0x8(SP)
          MOVQ DX, 0x10(SP)
          CALL main.Person.sayHello(SB)

          不同之處就在于這個(gè)CALL上,自動(dòng)解引用時(shí)的CALL其實(shí)是把指針指向的內(nèi)容視作普通類型,因此會(huì)去靜態(tài)查找方法進(jìn)行調(diào)用,而指向的內(nèi)容是 interface 的時(shí)候,編譯器會(huì)去 interface 本身的數(shù)據(jù)結(jié)構(gòu)上去查找有沒有Rap這個(gè)方法,答案顯然是沒有,所以爆了p.Rap undefined錯(cuò)誤。

          那么 interface 的真實(shí)長(zhǎng)相是什么呢,我們看看 go1.15.2 的實(shí)現(xiàn):

          //?src/runtime/runtime2.go
          //?因?yàn)檫@邊沒使用空接口,所以只節(jié)選了含數(shù)據(jù)接口的實(shí)現(xiàn)
          type?iface?struct?{
          ?tab??*itab
          ?data?unsafe.Pointer
          }

          //?src/runtime/runtime2.go
          type?itab?struct?{
          ?inter?*interfacetype
          ?_type?*_type
          ?hash??uint32?//?copy?of?_type.hash.?Used?for?type?switches.
          ?_?????[4]byte
          ?fun???[1]uintptr?//?variable?sized.?fun[0]==0?means?_type?does?not?implement?inter.
          }

          //?src/runtime/type.go
          type?imethod?struct?{
          ?name?nameOff
          ?ityp?typeOff
          }

          type?interfacetype?struct?{
          ?typ?????_type
          ?pkgpath?name
          ?mhdr????[]imethod?//?類型所包含的全部方法
          }

          //?src/runtime/type.go
          type?_type?struct?{
          ?size???????uintptr
          ?ptrdata????uintptr?//?size?of?memory?prefix?holding?all?pointers
          ?hash???????uint32
          ?tflag??????tflag
          ?align??????uint8
          ?fieldAlign?uint8
          ?kind???????uint8
          ?//?function?for?comparing?objects?of?this?type
          ?//?(ptr?to?object?A,?ptr?to?object?B)?->?==?
          ?equal?func(unsafe.Pointer,?unsafe.Pointer)?bool
          ?//?gcdata?stores?the?GC?type?data?for?the?garbage?collector.
          ?//?If?the?KindGCProg?bit?is?set?in?kind,?gcdata?is?a?GC?program.
          ?//?Otherwise?it?is?a?ptrmask?bitmap.?See?mbitmap.go?for?details.
          ?gcdata????*byte
          ?str???????nameOff
          ?ptrToThis?typeOff
          }

          沒有給出定義的類型都是對(duì)各種整數(shù)類型的 typing alias。interface實(shí)際上就是存儲(chǔ)類型信息和實(shí)際數(shù)據(jù)的struct,自動(dòng)解引用后編譯器是直接查看內(nèi)存內(nèi)容的(見匯編),這時(shí)看到的其實(shí)是iface這個(gè)普通類型,所以靜態(tài)查找一個(gè)不存在的方法就失敗了。而為什么手動(dòng)解引用的代碼可以運(yùn)行?因?yàn)槲覀兪謩?dòng)解引用后編譯器可以推導(dǎo)出實(shí)際類型是 interface,這時(shí)候編譯器就很自然地用處理 interface 的方法去處理它而不是直接把內(nèi)存里的東西尋址后塞進(jìn)寄存器。

          總結(jié)

          其實(shí)也沒什么好總結(jié)的。只有兩點(diǎn)需要記住,一是 interface 是有自己對(duì)應(yīng)的實(shí)體數(shù)據(jù)結(jié)構(gòu)的,二是盡量不要用指針去指向 interface,因?yàn)?golang 對(duì)指針自動(dòng)解引用的處理會(huì)帶來陷阱。

          如果你對(duì) interface 的實(shí)現(xiàn)很感興趣的話,這里有個(gè) reflect+暴力窮舉實(shí)現(xiàn)的乞丐版[4]

          理解了乞丐版的基礎(chǔ)上如果有興趣還可以看看真正的 golang 實(shí)現(xiàn),數(shù)據(jù)的層次結(jié)構(gòu)上更細(xì)化,而且有使用指針和內(nèi)存偏移等的聰明辦法,不說是否會(huì)有收獲,起碼研究起來不會(huì)無聊:P。

          參考資料

          [1]

          @apocelipes: https://github.com/apocelipes

          [2]

          go 語言愛好者周刊: https://www.zhihu.com/column/polaris

          [3]

          Go 101: https://go101.org/article/101.html

          [4]

          乞丐版: https://www.tapirgames.com/blog/golang-interface-implementation



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。


          瀏覽 98
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  c逼视频香蕉视频 | 在线国产中文字幕 | 大香伊人网 | 欧美成人精品H | 少妇被躁高潮内谢了 |