android 集成同一interface不同泛型_Dig101:Go之读懂interface的底层设计
Dig101: dig more, simplified more and know more
今天我們聊聊萬(wàn)物皆可為的接口(interface)底層設(shè)計(jì)。
interface 被定義為一組方法的簽名。
有了它,我們可以訂立方法契約,去抽象和約束實(shí)現(xiàn)。
而 Go 的基礎(chǔ)類型,可以認(rèn)為是沒(méi)有實(shí)現(xiàn)任何方法的空 interface,也就是萬(wàn)物皆為的 interface。
(Go 語(yǔ)言沒(méi)有泛型,接口可以作為一種替代實(shí)現(xiàn))
接口也被寄予厚望,主力開(kāi)發(fā) Russ Cox 曾說(shuō)過(guò):
從語(yǔ)言設(shè)計(jì)的角度來(lái)看,Go 的接口是靜態(tài)的,在編譯時(shí)檢查過(guò)的,在需要時(shí)是動(dòng)態(tài)的。如果我可以將 Go 的一個(gè)特性導(dǎo)出到其他語(yǔ)言中,那就是接口。Go Data Structures: Interfaces[1]
那到底 interface 是怎么設(shè)計(jì)的底層結(jié)構(gòu)呢?
又怎么支持的duck typing[2]?
在類型斷言時(shí)又發(fā)生了什么?
帶著這些問(wèn)題,我們往下看
文章目錄
0x01 底層結(jié)構(gòu)一樣么
eface
iface
0x02 類型如何相互轉(zhuǎn)換
convXXX 的命名
起初的 convT2{I,E} 和 convI2I
針對(duì)類型優(yōu)化后的 convXXX
0x03 類型斷言如何實(shí)現(xiàn)
查表是否匹配
嘗試插入更新
動(dòng)態(tài)判定效率優(yōu)化
0x01 底層結(jié)構(gòu)一樣么
我們知道定義接口有這兩種方式,那他們底層結(jié)構(gòu)是一樣的么?
// 方式1var a interface{}
// 方式2
type Stringer interface {
String() string
}
var b Stringer
答案是【不一樣】
我們用 gdb 打印下對(duì)應(yīng)類型(gdb 相關(guān)見(jiàn) Tips-如何優(yōu)雅的使用GDB調(diào)試Go)
// 空接口類型// 有函數(shù)定義的接口類型// itable相關(guān)類型以此可見(jiàn) Go 內(nèi)部定義了兩種 interface(但都是兩個(gè)機(jī)器字)
eface
空接口,指沒(méi)有定義方法的接口
內(nèi)部存儲(chǔ)了構(gòu)造類型(concrete type)type和data
efaceiface
有方法的接口
有了相比eface的type更豐富的itab字段,其中記錄了構(gòu)造類型及所實(shí)現(xiàn)的 interface 類型的類型和方法
iface0x02 類型如何相互轉(zhuǎn)換
如下代碼,當(dāng)我們做接口賦值時(shí),Go 又會(huì)怎樣填充底層結(jié)構(gòu)呢?
type Binary uint64func (i Binary) String() string {
return strconv.Itoa(int(i))
}
func conversion() {
var b Stringer
var i Binary = 1
b = i // <= 這里發(fā)生了什么
println(b.String())
}
gdb 進(jìn)到 b = i 這一步,會(huì)發(fā)現(xiàn)他調(diào)用了runtime/iface.go:convT64方法實(shí)現(xiàn) iface 的賦值
查閱源碼,會(huì)發(fā)現(xiàn)很多convXXX函數(shù), 他們是干什么的?
convXXX 的命名
convFrom2To 指代 To=From 的轉(zhuǎn)換
From 和 To 的類型有三種:(參見(jiàn)cmd/compile/internal/types/type.go:Tie)
- E (eface)
- I (iface)
- T (Type)
這一堆函數(shù)看的人眼暈,但參照提交specialize convT2x, don't alloc for zero vals[3]深入分析,就會(huì)清晰許多
起初的 convT2{I,E} 和 convI2I
最初只有 convT2{I,E} 和 convI2I
主要實(shí)現(xiàn)分配內(nèi)存(newobject),然后拷貝賦值(typedmemmove)
convI2I 還會(huì)有g(shù)etitab, 具體是什么我們后邊類型斷言時(shí)說(shuō)
然后也在調(diào)用他們前(walkexpr)做了優(yōu)化
- 減少值拷貝
ToType 為類指針(pointer-shaped)或者一個(gè)機(jī)器字內(nèi)(int)的話,可以直接存入 interface 的 data 字段(主要優(yōu)化在這里)
pointer-shaped類型: ptr, chan, map, func, unsafe.Pointer
再輔以 type 的存儲(chǔ),就只是兩個(gè)字(two-word)的拷貝
- 減少內(nèi)存分配
零值,bool/byte 可以不用分配內(nèi)存,而用已存在值(zerobase,staticbytes)
只讀的全局變量(readonly global)直接可以用
1kb 以內(nèi),不escape到堆上,非interface的變量可以使用棧上分配的臨時(shí)變量(stack temporary initialized)
這類 value 最后以取地址形式轉(zhuǎn)化為 interface:{type/itab, &value}.
- interface 轉(zhuǎn)空接口(eface)
可以丟棄除type以外的itab
tmp = i.itabif tmp != nil {
tmp = tmp.type
}
e = iface{tmp, i.data}
針對(duì)類型優(yōu)化后的 convXXX
但這里會(huì)有一些可以優(yōu)化的點(diǎn),如:
- 分配內(nèi)存是否可以需要清零?
類指針的類型需要清零,不然內(nèi)存可能有臟數(shù)據(jù)
但無(wú)指針類型(pointer-free)如拷貝時(shí)直接可以覆蓋對(duì)應(yīng)內(nèi)存則不需要
如int其拷貝在一個(gè)機(jī)器字內(nèi)完成,不需要分配時(shí)清零 (32 位系統(tǒng)上不調(diào)用convT64,就可以保證訪問(wèn)內(nèi)存是安全的原子操作)
- 是否可以簡(jiǎn)化值拷貝?
int,string,slice這些 Type 分配的x拷貝val時(shí),可以簡(jiǎn)化為 *(*Type)(x) = val
- 拷貝內(nèi)存是否可以不增加 gc 調(diào)用(寫(xiě)屏障)?
按 ToType 類型是否含指針區(qū)分 類指針類型(pointer-shaped): convT2{E,I} 需要拷貝時(shí) gc 調(diào)用(typedmemmove)
無(wú)指針類型(pointer-free): convT2{E,I}noptr 不需要拷貝時(shí) gc 調(diào)用(memmove)
這樣一看就明白這些函數(shù)的用意了,還是為了針對(duì)性的提高轉(zhuǎn)化效率
最后結(jié)合其調(diào)用處convXXX列表如下:
// cmd/compile/internal/gc/walk.go:walkexprcase OCONVIFACE:
...
fnname, needsaddr := convFuncName(fromType, toType)
| convI2I | iface | 否 |
| convT{16, 32,64} | 整型數(shù)據(jù) (無(wú)指針, 機(jī)器字內(nèi)) | 否 |
| convTstring | string | 否 |
| convTslice | slice | 否 |
| convT2E | Type | 是 |
| convT2Enoptr | 無(wú)指針 Type | 是 |
| convT2I | Type | 是 |
| convT2Inoptr | 無(wú)指針 Type | 是 |
不會(huì)存在 convE2E 和 convE2I?
needsaddr: 類型不含指針,大小大于 64 位字或未知大小時(shí),使用值的地址來(lái)存
0x03 類型斷言如何實(shí)現(xiàn)
interface 支持類型斷言,來(lái)動(dòng)態(tài)判斷其構(gòu)造類型,
判定成功可返回對(duì)應(yīng)構(gòu)造類型,便于調(diào)用其方法
可構(gòu)造類型實(shí)現(xiàn) interface 不需要顯示聲明,
那如下代碼是怎么確定 interface b(構(gòu)造類型是Binary)實(shí)現(xiàn)Stringer呢?
type Binary uint64func (i Binary) String() string {
return fmt.Sprint(i)
}
func typeAssert() {
var b interface{} = Binary(1)
v, ok := b.(Stringer)
println(v, ok)
}
調(diào)試后會(huì)發(fā)現(xiàn),其調(diào)用了assertE2I2
這里函數(shù)命名有兩類,如下
assertE2I: v := eface1.(iface1)
assertE2I2: v,ok := eface1.(iface1)
這里有一點(diǎn),類型斷言非 v,ok 方式的,斷言失敗會(huì) panic)
原來(lái)其內(nèi)部進(jìn)行了itab表(itabTable)查詢 interface 和構(gòu)造類型的映射表,如果匹配則說(shuō)明實(shí)現(xiàn)
下邊代碼分析如下
首先初始 512 個(gè) entry 的表
const itabInitSize = 512type itabTableType struct {
// 上限
size uintptr
// 當(dāng)前用量
count uintptr
entries [itabInitSize]*itab
}
查表是否匹配
在類型斷言中調(diào)用 getitab(inter, typ, canfail) 查表
- 先不加鎖 atomic 讀取 itabTable,找到返回
- 未找到加鎖再查一遍,找到返回
- 還沒(méi)有就創(chuàng)建一個(gè) itab 添加到表中,添加完后解鎖
- 期間如果判定不匹配則按是否可以 panic(canfail)返回
其中查表用到 itabTable.find(inter, typ),
插入用到 itabAdd(m)
嘗試插入更新
- 插入前需先用m.inter/m._type pair 初始化 m.fun 數(shù)組,不匹配則m.fun[0]==0
(m.fun 類型 [1]uintptr,實(shí)際指向是大小為接口定義方法數(shù)的方法數(shù)組。詳見(jiàn) func (m *itab) init())
用量 count 超過(guò)上限的 75%觸發(fā)擴(kuò)容,大小為 2 倍以上(要向上內(nèi)存對(duì)齊),擴(kuò)容后更新 itabTable 是原子操作
以 itab m 的 interface 類型和構(gòu)造類型的 hash 計(jì)算對(duì)應(yīng) itabTable 的起始偏移,然后插入到其后第一個(gè)不為空的 entry。如果已存在則直接返回
這里用到了開(kāi)放地址探測(cè)法,公式是:
h(i) = h0 + i*(i+1)/2 mod 2^k
具體插入用到 itabTable.add(m)
這里和其實(shí) map 插入的邏輯很相似
動(dòng)態(tài)判定效率優(yōu)化
不過(guò),這里有一個(gè)問(wèn)題?
假定,interface 定義了ni個(gè)方法,構(gòu)造類型實(shí)現(xiàn)nt個(gè)方法,
常規(guī)匹配構(gòu)造類型是否實(shí)現(xiàn)全部ni個(gè)方法需要兩層遍歷,復(fù)雜度為O(ni*nt)
這樣在初始化itab.fun或類型斷言匹配時(shí)效率會(huì)比較低。
Go 設(shè)計(jì)時(shí)也考慮了這個(gè)問(wèn)題,把復(fù)雜度降低為O(ni+nt)
這也是使用 hashtable 的原因之一:
首先 interface 的函數(shù)定義列表itab.inter.mhdr和構(gòu)造類型的函數(shù)列表itab.fun都是按函數(shù)名排好序的
這樣第一次 itab 初始化時(shí),判定構(gòu)造類型是否實(shí)現(xiàn)函數(shù)列表可以O(shè)(ni+nt)內(nèi)遍歷完成
然后用開(kāi)放地址探測(cè)法更新到 itabtable 中,查詢時(shí)也可以用同樣的方式定位到此 itab 是否存在。
兩個(gè)(有序)列表的遍歷匹配代碼精簡(jiǎn)如下:
// runtime/iface.go:init()j:=0
imethods:
// 遍歷interface定義函數(shù)列表
for k := 0; k < ni; k++ {
// 遍歷構(gòu)造類型函數(shù)列表
for ; j < nt; j++ {
// 如果兩者類型(type),包路徑(pkgpath),函數(shù)名(name)匹配
if xxx {
// 將方法記錄到fun0(最終全匹配則賦值給 m.fun)
continue imethods
}
}
// 未全匹配
m.fun[0] = 0
}
m.fun[0] = uintptr(fun0)
總結(jié)一下 interface 的底層設(shè)計(jì):
- interface 分為空接口(eface)和接口(iface)兩類,但都是兩機(jī)器字(two-word)存儲(chǔ)結(jié)構(gòu)
- interface 轉(zhuǎn)換中針對(duì)不同類型做了優(yōu)化,主要集中于提升內(nèi)存分配和值拷貝效率
- interface 類型斷言時(shí)動(dòng)態(tài)判定,利用有序列表遍歷+全局哈希表表緩存優(yōu)化判定效率
See More:官方解釋?InterfaceSlice[4]?為什么不能直接轉(zhuǎn)化
最后留個(gè)問(wèn)題:
下邊這段轉(zhuǎn)換代碼內(nèi)部沒(méi)有調(diào)convT64,為什么?
var b Stringer = Binary(1)_ = b.String()
這個(gè)問(wèn)題下一篇文章再來(lái)給出解答。
本文代碼見(jiàn) NewbMiao/Dig101-Go[5]
參考資料
[1]Go Data Structures: Interfaces: https://research.swtch.com/interfaces
[2]duck typing: https://en.wikipedia.org/wiki/Duck_typing
[3]specialize convT2x, don't alloc for zero vals: https://go-review.googlesource.com/c/go/+/36476
[4]InterfaceSlice: https://github.com/golang/go/wiki/InterfaceSlice
[5]NewbMiao/Dig101-Go: https://github.com/NewbMiao/Dig101-Go/blob/master/types/interface/interface.go
推薦閱讀
- Dig101: Go之for-range排坑指南
- Dig101: Go之靈活的slice
- Dig101: Go之string那些事
- Dig101:Go之讀懂map的底層設(shè)計(jì)
- Dig101:Go之聊聊struct的內(nèi)存對(duì)齊
- Tips-如何優(yōu)雅的使用GDB調(diào)試Go
原創(chuàng)不易,如果有用,歡迎轉(zhuǎn)發(fā)、點(diǎn)個(gè)在看,分享給更多人。
微信內(nèi)外鏈不能跳轉(zhuǎn),戳閱讀原文查看原文中參考資料
????歡迎關(guān)注我,一位愛(ài)折騰的開(kāi)發(fā)(奶爸):熱衷搬磚、價(jià)投。
總結(jié)
以上是生活随笔為你收集整理的android 集成同一interface不同泛型_Dig101:Go之读懂interface的底层设计的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: PHP8 的 JIT 是什么?
- 下一篇: 线程池参数详解_java中常见的六种线程