golang被詬病最多的,沒有泛型應該算一個。做爲強類型語言來講,沒有泛型不少時候在業務開發上會有些不適應,可是它有個interface
類型,被不少人拿來當泛型玩,若是你瞭解它的原理也是沒問題的。 可是你真的瞭解嗎?golang
golang
中的interface
,能夠將任意類型的變量賦予它。常見的咱們區分兩種,一種就是struct
類型的,由於struct
可能會有func
;另一種,就是非結構體的普通類型(下面提到的普通類型,都是指代除struct
外的類型)安全
1 package main
2
3 import "fmt"
4
5 func main() {
6 var x int
7 var y interface{}
8 x = 1
9 y = x
10 fmt.Println(y)
11 }
複製代碼
當咱們把int
類型的變量賦值給interface
類型時,會發生什麼:bash
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
main.go:5 0x4a23a0 64488b0c25f8ffffff mov rcx, qword ptr fs:[0xfffffff8]
main.go:5 0x4a23a9 488d4424f8 lea rax, ptr [rsp-0x8]
main.go:5 0x4a23ae 483b4110 cmp rax, qword ptr [rcx+0x10]
main.go:5 0x4a23b2 0f86c7000000 jbe 0x4a247f
=> main.go:5 0x4a23b8* 4881ec88000000 sub rsp, 0x88
main.go:5 0x4a23bf 4889ac2480000000 mov qword ptr [rsp+0x80], rbp
main.go:5 0x4a23c7 488dac2480000000 lea rbp, ptr [rsp+0x80]
main.go:6 0x4a23cf 48c744243000000000 mov qword ptr [rsp+0x30], 0x0
main.go:7 0x4a23d8 0f57c0 xorps xmm0, xmm0
main.go:7 0x4a23db 0f11442448 movups xmmword ptr [rsp+0x48], xmm0
main.go:8 0x4a23e0 48c744243001000000 mov qword ptr [rsp+0x30], 0x1
main.go:9 0x4a23e9 48c7042401000000 mov qword ptr [rsp], 0x1
main.go:9 0x4a23f1 e89a70f6ff call $runtime.convT64
複製代碼
追到runtime
的convT64
方法,一探究竟。併發
// type uint64InterfacePtr uint64
// var uint64Eface interface{} = uint64InterfacePtr(0)
// var uint64Type *_type = (*eface)(unsafe.Pointer(&uint64Eface))._type
func convT64(val uint64) (x unsafe.Pointer) {
if val == 0 {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(8, uint64Type, false)
*(*uint64)(x) = val
}
return
}
複製代碼
這個方法返回了 val
的指針,其中uint64Type
就是一個 0 值的uint64
指針。有個疑問,這裏uint64Type
定義時,eface
是什麼:函數
type eface struct {
_type *_type
data unsafe.Pointer
}
複製代碼
這個結構體,剛好知足了,對於普通類型轉換interface
,或者說是將普通類型賦值給interface
所必須的兩個字段,當前類型的type
和值
(這裏貌似有點繞口)。真實的是,eface
確實就是表示這類interface
的結構體,在runtime
中,還能看到其餘普通類型的轉換, convTslice
、convTstring
、convT64
、convT32
等其餘幾個方法。ui
若是是一個擁有func
的struct
類型的變量,賦值給另外一個interface
,這類的interface
在底層是怎麼存的呢。以下所示:atom
1 package main
2
3 import "fmt"
4
5 type Human interface{ Introduce() string }
6
7 type Bob struct{ Human }
8
9 func (b Bob) Introduce() string { return "Name: Bob" }
10
11 func main() {
12 var y Human
13 x := Bob{}
14 y = x
15 fmt.Println(y)
16 }
複製代碼
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
main.go:11 0x10b71a0 65488b0c2530000000 mov rcx, qword ptr gs:[0x30]
main.go:11 0x10b71a9 488d4424d0 lea rax, ptr [rsp-0x30]
main.go:11 0x10b71ae 483b4110 cmp rax, qword ptr [rcx+0x10]
main.go:11 0x10b71b2 0f860f010000 jbe 0x10b72c7
...省略部分指令
main.go:14 0x10b7202 e84921f5ff call $runtime.convT2I
複製代碼
看彙編代碼,在 16
行時,調用了runtime.convT2I
,這個方法返回的類型是iface
spa
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}
複製代碼
itab
包括具體值的type
和 interface 的type
,還有其餘字段3d
type itab struct {
inter *interfacetype // 接口定義的類型
_type *_type // 接口指向具體值的 type
hash uint32 // 類型的hash值
_ [4]byte
fun [1]uintptr // 判斷接口是否實現全部方法(下面會講到)
}
複製代碼
在itab
結構體的init
方法中,是全部字段的初始化,重點看這個方法:指針
func (m *itab) init() string {
inter := m.inter
typ := m._type
x := typ.uncommon()
// 在 interfacetype 的結構體中,mhdr 存着全部須要實現的方法的
// 結構體切片 []imethod,都是按照方法名的字典序排列的,其中:
// ni 是全量的方法(全部要實現的方法)的個數
// nt 是已實現的方法的個數
ni := len(inter.mhdr)
nt := int(x.mcount)
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
j := 0
methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
var fun0 unsafe.Pointer
imethods:
for 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)
// 比較已實現方法的 type 和 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 {
// 計算每一個 method 對應代碼塊的內存地址
ifn := typ.textOff(t.ifn)
if k == 0 {
fun0 = ifn // we'll set m.fun[0] at the end } else { methods[k] = ifn } } continue imethods } } } // 若是沒有找到,將 func[0] 設置爲0,返回該實現的 method 的 name m.fun[0] = 0 return iname } // 第一個方法的 ptr 和 type 的 hash m.fun[0] = uintptr(fun0) m.hash = typ.hash return "" } 複製代碼
還有一種將interface
類型的實現,賦值給另一個interface
:
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
...省略部分指令
main.go:18 0x10b71f5 488d842480000000 lea rax, ptr [rsp+0x80]
main.go:18 0x10b71fd 4889442408 mov qword ptr [rsp+0x8], rax
main.go:18 0x10b7202 e84921f5ff call $runtime.convT2I
複製代碼
func convI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
if tab == nil {
return
}
if tab.inter == inter {
r.tab = tab
r.data = i.data
return
}
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}
複製代碼
經過前面的分析,咱們又知道, iface
是由 tab
和 data
兩個字段組成。因此,實際上 convI2I
函數真正要作的事, 找到新 interface
的 tab
和 data
,就大功告成了。在iface.go
文件頭部定義了itabTable
全局哈希表存全部itab
, 其實就是空間換時間
的思想。
itabTable
是itabTableType
結構體(個人golang版本是1.12.7)
type itabTableType struct {
size uintptr // 大小,2的冪
count uintptr // 已有的 itab entry 個數
entries [itabInitSize]*itab // 保存 itab entry
}
複製代碼
getitab
是查找itab
的方法
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
if len(inter.mhdr) == 0 {
throw("internal error - misuse of itab")
}
if typ.tflag&tflagUncommon == 0 {
if canfail {
return nil
}
name := inter.typ.nameOff(inter.mhdr[0].name)
panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
}
var m *itab
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
if m = t.find(inter, typ); m != nil {
goto finish
}
// Not found. Grab the lock and try again.
lock(&itabLock)
if m = itabTable.find(inter, typ); m != nil {
unlock(&itabLock)
goto finish
}
// Entry doesn't exist yet. Make a new entry & add it. m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys)) m.inter = inter m._type = typ m.init() itabAdd(m) unlock(&itabLock) finish: if m.fun[0] != 0 { return m } if canfail { return nil } // 若是不是 "_, ok := " 類型的斷言,會有panic panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()}) } 複製代碼
會調用find
方法,根據interfacetype
和_type
的 hash 值,在itabTable
中查找,找到的話直接返回; 不然,生成新的itab
,加入 itabTable
中。有個問題,就是爲何第一次不加鎖找,而第二次加鎖?
我我的的理解是:首先:應該仍是想避免鎖的開銷(以前在滴滴有幸聽過曹大分享【內存重排】,對經常使用package在concurrently時,鎖引發的問題作了一些分析。), 而第二次加鎖,我以爲更多的是在未找到 itab 後,會新生成一個 itab 寫入全局哈希表中,若是有其餘協程在查詢時,也未找到,能夠併發安全寫入。
func itabAdd(m *itab) {
if getg().m.mallocing != 0 {
throw("malloc deadlock")
}
t := itabTable
if t.count >= 3*(t.size/4) { // 75% load factor
t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
t2.size = t.size * 2
iterate_itabs(t2.add)
if t2.count != t.count {
throw("mismatched count during itab table copy")
}
atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
t = itabTable
}
t.add(m)
}
複製代碼
itabAdd
是添加itab
加入itabTable
的方法。既然是hash
表,就必定會發生擴容
。每次都 是2
的倍數的增加,建立新的 itabTable
再原子
的替換。在 iterate_itabs
(複製)時,並 未加鎖,這裏不是協程安全的,而是在添加前,在getitab
方法中有鎖的操做,會等待複製完成。