[TOC]
本文基於golang 1.11源碼進行分析。先演示用法和注意事項,再深入源碼分析原理。
在golang中,接口本身也是一種類型,它代表的是一個方法的集合。任何類型只要實現了接口中聲明的所有方法,那麼該類就實現了該接口。與其他語言不同,golang並不需要顯示聲明類型實現了某個接口,而是由編譯器和runtime進行檢查。不用顯示什麼這點非常棒,這樣就無侵入非常方便。
1 使用
1.1 聲明
type 接口名 interface {
方法1
方法2
...
方法n
}
type 接口名 interface {
已聲明接口名1
...
已聲明接口名n
}
如果一個接口不包含任何方法,那麼就是一個空接口(empty interface),所有類型都符合empty interface的定義,因此任何類型都能轉換成empty interface,可以看到常常使用empty interface來實現多態,例如之前我們分析過的map源碼。
對於接口的命名,一般我們都是以er結尾,例如Writer、Reader等等。
1.2 實現接口
我們看個例子:
package main
import (
"fmt"
)
type Tester interface {
Display()
DisplayAppend(string)
DisplayAppend2(string) string
}
type Tester2 interface {
DisplayAppend(string)
}
type Test struct {
s string
}
func (t *Test) Display() {
fmt.Printf("Display:%p, %#v\n", t ,t)
}
func (t Test) DisplayAppend(s string) {
t.s += s
fmt.Printf("DisplayAppend:%p, %#v\n", &t, t)
}
func (t *Test) DisplayAppend2 (s string) string {
t.s += s
fmt.Printf("DisplayAppend2:%p, %#v\n", t, t)
return t.s
}
func TestInterface(t Tester) {
t.Display()
t.DisplayAppend(" TestInterface")
t.DisplayAppend2(" TestInterface")
}
func TestInterface2(t Tester2) {
t.DisplayAppend("TestInterface2")
}
func main() {
var test Test
test.s = "aaa"
fmt.Printf("%p\n", &test)
test.Display()
test.DisplayAppend(" raw")
TestInterface(&test)
//TestInterface(test) //cannot use test (type Test) as type Tester in argument to TestInterface:Test does not implement Tester (Display method has pointer receiver)
TestInterface2(&test)
TestInterface2(test)
}
輸出
0xc42000e1e0
Display:0xc42000e1e0, &main.Test{s:"aaa"}
DisplayAppend:0xc42000e200, main.Test{s:"aaa raw"}
Display:0xc42000e1e0, &main.Test{s:"aaa"}
DisplayAppend:0xc42000e230, main.Test{s:"aaa TestInterface"}
DisplayAppend2:0xc42000e1e0, &main.Test{s:"aaa TestInterface"}
DisplayAppend:0xc42000e260, main.Test{s:"aaa TestInterfaceTestInterface2"}
DisplayAppend:0xc42000e290, main.Test{s:"aaa TestInterfaceTestInterface2"}
在這個例子中,我們定義了一個類型Test,Test類型有三個方法,兩個方法的接受者是*Test,一個方法的接受者是Test;定義兩個接口類型,Tester和Tester2 ,Tester有三個Test中的同名方法,Tester2中只有接受者是Test的同名方法。然後有TestInterface函數,入參是Tester,TestInterface2函數入參是Tester2。
從編譯和運行結果可以看到,TestInterface函數入參只能填Test類型,TestInterface2入參既可以是Test也可以是*Test。TestInterface傳入Test類型變量test時,編譯報錯:
cannot use test (type Test1) as type Tester in argument to TestInterface:Test1 does not implement Tester (Display method has pointer receiver)
意思是說test並沒有實現Tester的Display接口,因爲golang中,類型T只有接受者是T的方法,語法中T能直接調*T的方法僅僅是語法糖;而類型*T擁有接受者是T和*T的方法。
1.3 類型判斷
傳入參數接口的時候,如果我們希望確切知道它具體類型,那麼就要用到類型判斷了。有兩種類型判斷方法:
變量.(類型) //判斷是不是某個具體類型
switch 變量.(type) //返回具體類型,必須搭配swith語句
talk is cheap,看代碼:
package main
import (
"fmt"
)
type Tester interface {
Display()
DisplayAppend(string)
DisplayAppend2(string) string
}
type Tester2 interface {
DisplayAppend(string)
}
type Test1 struct {
s string
}
func (t *Test1) Display() {
fmt.Printf("Display:%p, %#v\n", t ,t)
}
func (t Test1) DisplayAppend(s string) {
t.s += s
fmt.Printf("DisplayAppend:%p, %#v\n", &t, t)
}
func (t *Test1) DisplayAppend2 (s string) string {
t.s += s
fmt.Printf("DisplayAppend2:%p, %#v\n", t, t)
return t.s
}
func TestInterface(t Tester) {
t.Display()
t.DisplayAppend(" TestInterface")
t.DisplayAppend2(" TestInterface")
}
func TestInterface2(t Tester2) {
t.DisplayAppend("TestInterface2")
}
func Printf(t interface{}) {
if v, ok := t.(int); ok {
v = 2
fmt.Printf("type[%T] %v %v\n", v, v, t)
}
if v, ok := t.(int32);ok {
fmt.Printf("type[%T] %v\n", v, v)
}
if v, ok := t.(int64); ok {
fmt.Printf("type[%T] %v\n", v, v)
}
if v, ok := t.(Tester2); ok {
fmt.Printf("type[%T] %v\n", v, v)
}
if v, ok := t.(Tester); ok {
fmt.Printf("type[%T] %v\n", v, v)
}
}
func Printf2(v interface{}) {
fmt.Printf("%p %v\n", &v, v)
switch v := v.(type) {
case nil:
fmt.Printf("type[%T] %v\n", v, v)
case int:
fmt.Printf("%p %v\n", &v, v)
fmt.Printf("type[%T] %v\n", v, v)
case int64:
fmt.Printf("type[%T] %v\n", v, v)
case string:
fmt.Printf("type[%T] %v\n", v, v)
case Tester:
fmt.Printf("tester type[%T] %v\n", v, v)
case Tester2:
fmt.Printf("tester2 type[%T] %v\n", v, v)
default:
fmt.Printf("unknow\n")
}
}
func main() {
var i int64 = 1
Printf(i)
var i2 int = 1
Printf(i2)
var test Test1
Printf(test)
Printf(&test)
fmt.Printf("------------\n")
Printf2(i2)
Printf2(test)
Printf2(&test)
}
輸出
type[int64] 1
type[int] 2 1
type[main.Test1] {}
type[main.Test1] &{}
type[main.Test1] &{}
------------
0xc42000e220 1
0xc4200160b8 1
type[int] 1
0xc42000e240 {}
tester2 type[main.Test1] {}
0xc42000e250 &{}
tester type[*main.Test1] &{}
從這裏我們可以看出兩個點:
- 對於判斷接口,只要實現該接口就能匹配上
- 類型判斷返回也是按值複製,修改返回的value,不影響原來的值
- 接口的匹配,是嚴格的匹配,並不是說接口1能轉換成接口2他們就能匹配上
golang中,我們經常用類型判斷來判斷特定的錯誤。
1.4 接口的值
接口的值簡單來說,是由兩部分組成的,就是類型和數據,詳細的組成會在下面的實現章節中說明。
那麼判斷兩個接口是相等,就是看他們的這兩部分是否相等;另外類型和數據都爲nil才代表接口是nil,eg:
var a interface{}
var b interface{} = (*int)(nil)
fmt.Println(a == nil, b == nil) //true false
這點很重要,很多人在實現error接口的時候判斷錯誤,下面我們看個例子:
type MyError struct{}
func (*MyError) Error() string {
return "my error"
}
func TestError(x int) (int, error) {
var err *MyError
if x < 0 {
err = new(MyError)
}
return x, err
}
func main() {
var err error
_, err = TestError(10)
fmt.Println(err == nil) //false
}
在x大於0時,TestError中的err是nil,返回的時候,轉換成error類型,那麼類型就是(*MyError),值是nil,由於類型不是nil,所以最終返回給調用方的總是失敗。
2 實現
如前面所說,golang中你不需要聲明一個類型實現了那些接口,這帶來了方便,但是實現上會比那些需要聲明的語言更加複雜。golang的接口檢測既有靜態部分,也有動態部分。
靜態部分
對於具體類型(concrete type,包括自定義類型) -> interface,編譯器生成對應的itab放到ELF的.rodata段,後續要獲取itab時,直接把指針指向存在.rodata的相關偏移地址即可。具體實現可以看golang的提交日誌CL 20901、CL 20902。
對於interface->具體類型(concrete type,包括自定義類型),編譯器提取相關字段進行比較,並生成值動態部分
在runtime中會有一個全局的hash表,記錄了相應type->interface類型轉換的itab,進行轉換時候,先到hash表中查,如果有就返回成功;如果沒有,就檢查這兩種類型能否轉換,能就插入到hash表中返回成功,不能就返回失敗。注意這裏的hash表不是go中的map,而是一個最原始的使用數組的hash表,使用開放地址法來解決衝突。主要是interface <-> interface(接口賦值給接口、接口轉換成另一接口)使用到動態生產itab。
2.1 結構
2.1.1 接口類型的結構 interfacetype
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
// imethod represents a method on an interface type
type imethod struct {
name nameOff // name of method
typ typeOff // .(*FuncType) underneath
}
pkgpath記錄定義接口的包名
其中的mdhr字段,是一個imethod切片,記錄接口中定義的那些函數。
nameOff 和 typeOff 類型是 int32 ,這兩個值是鏈接器負責嵌入的,相對於可執行文件的元信息的偏移量。元信息會在運行期,加載到 runtime.moduledata 結構體中。
2.1.2 接口值的結構 iface eface
爲了性能,golang專門分了兩種interface,eface和iface,eface就是空接口,iface就是有方法的接口
結構定義分別在兩個文件中,runtime2.go:
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
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.
}
data字段是用來存儲實際數據的,runtime會申請一塊新的內存,把數據考到那,然後data指向這塊新的內存。
eface、itab的_tpye字段,是data指向的值的實際類型信息。
iface中tab字段,是一個itab結構,包含了inter接口類型、_type數據類型、hash哈希的方法、fun函數地址佔位符。這個hash方法拷貝自_type.hash;fun是一個大小爲1的uintptr數組,當fun[0]爲0時,說明_type並沒有實現該接口,當有實現接口時,fun存放了第一個接口方法的地址,其他方法一次往下存放,這裏就簡單用空間換時間,其實方法都在_type字段中能找到,實際在這記錄下,每次調用的時候就不用動態查找了。
2.1.2 全局的itab table
iface.go:
const itabInitSize = 512
// Note: change the formula in the mallocgc call in itabAdd if you change these fields.
type itabTableType struct {
size uintptr // length of entries array. Always a power of 2.
count uintptr // current number of filled entries.
entries [itabInitSize]*itab // really [size] large
}
可以看出這個全局的itabTable是用數組在存儲的
size記錄數組的大小,總是2的次冪。
count記錄數組中已使用了多少。
entries是一個*itab數組,初始大小是512.
2.2 轉換
把一個具體的值,賦值給接口,會調用conv系列函數,例如空接口調用convT2E系列、非空接口調用convT2I系列,爲了性能考慮,很多特例的convT2I64、convT2Estring諸如此類,避免了typedmemmove的調用。
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
// TODO: We allocate a zeroed object only to overwrite it with actual data.
// Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
typedmemmove(t, x, elem)
e._type = t
e.data = x
return
}
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
}
func convT2I16(tab *itab, val uint16) (i iface) {
t := tab._type
var x unsafe.Pointer
if val == 0 {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(2, t, false)
*(*uint16)(x) = val
}
i.tab = tab
i.data = x
return
}
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
}
可以看到:
- 具體類型轉空接口,_type字段直接複製源的type;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
- 具體類型轉非空接口,入參tab是編譯器生成的填進去的,接口指向同一個入參tab指向的itab;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
- 對於接口轉接口,itab是調用getitab函數去獲取的,而不是編譯器傳入的。
對於那些特定類型的值,如果是零值,那麼不會mallocgc一塊新內存,data會指向zeroVal[0]。
2.2.1 編譯器優化
每次都malloc一塊內存,那麼性能會很差,因此,對於一些類型,golang的編譯器做了優化。 TODO
2.3 獲取itab的流程
golang interface的核心邏輯就在這,在get的時候,不僅僅會從itabTalbe中查找,還可能會創建插入,itabTable使用容量超過75%還會擴容。下面我們看下代碼:
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
if len(inter.mhdr) == 0 {
throw("internal error - misuse of itab")
}
// easy case
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
// First, look in the existing table to see if we can find the itab we need.
// This is by far the most common case, so do it without locks.
// Use atomic to ensure we see any previous writes done by the thread
// that updates the itabTable field (with atomic.Storep in itabAdd).
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
}
// this can only happen if the conversion
// was already done once using the , ok form
// and we have a cached negative result.
// The cached result doesn't record which
// interface function was missing, so initialize
// the itab again to get the missing function name.
panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
流程很簡單
- 先用t保存全局itabTable的地址,然後使用t.find去查找,這樣是爲了防止查找過程中,itabTable被替換導致查找錯誤。
- 如果沒找到,那麼就會上鎖,然後使用itabTable.find去查找,這樣是因爲在第一步查找的同時,另外一個協程寫入,可能導致實際存在卻查找不到,這時上鎖避免itabTable被替換,然後直接在itaTable中查找。
- 再沒找到,說明確實沒有,那麼就根據接口類型、數據類型,去生成一個新的itab,然後插入到itabTable中,這裏可能會導致hash表擴容,如果數據類型並沒有實現接口,那麼根據調用方式,該報錯報錯,該panic panic。
這裏我們可以看到申請新的itab空間時,內存空間的大小是unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize,參照前面接受的結構,len(inter.mhdr)就是接口定義的方法數量,因爲字段fun是一個大小爲1的數組,所以len(inter.mhdr)-1,在fun字段下面其實隱藏了其他方法接口地址。
然後我們再看下上面用到的一些方法的細節
2.3.1 在itabTable中查找itab find
func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
// compiler has provided some good hash codes for us.
return uintptr(inter.typ.hash ^ typ.hash)
}
// find finds the given interface/type pair in t.
// Returns nil if the given interface/type pair isn't present.
func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
// Implemented using quadratic probing.
// Probe sequence is h(i) = h0 + i*(i+1)/2 mod 2^k.
// We're guaranteed to hit all table entries using this probe sequence.
mask := t.size - 1
h := itabHashFunc(inter, typ) & mask
for i := uintptr(1); ; i++ {
p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
// Use atomic read here so if we see m != nil, we also see
// the initializations of the fields of m.
// m := *p
m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
if m == nil {
return nil
}
if m.inter == inter && m._type == typ {
return m
}
h += I
h &= mask
}
}
從註釋我們可以看到,golang使用的開放地址探測法,用的是公式h(i) = h0 + i*(i+1)/2 mod 2^k,h0是根據接口類型和數據類型的hash字段算出來的。以前的版本是額外使用一個link字段去連到下一個slot,那樣會有額外的存儲,性能也會差寫,在1.11中我們看到做了改進,具體是哪個版本開始變的我也不知道。
2.3.2 檢查並生成itab init
// init fills in the m.fun array with all the code pointers for
// the m.inter/m._type pair. If the type does not implement the interface,
// it sets m.fun[0] to 0 and returns the name of an interface function that is missing.
// It is ok to call this multiple times on the same m, even concurrently.
func (m *itab) init() string {
inter := m.inter
typ := m._type
x := 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).
ni := len(inter.mhdr)
nt := int(x.mcount)
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
j := 0
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)
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 {
ifn := typ.textOff(t.ifn)
*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
}
continue imethods
}
}
}
// didn't find method
m.fun[0] = 0
return iname
}
m.hash = typ.hash
return ""
}
這個方法會檢查interface和type的方法是否匹配,即type有沒有實現interface。假如interface有n中方法,type有m中方法,那麼匹配的時間複雜度是O(n x m),由於interface、type的方法都按字典序排,所以O(n+m)的時間複雜度可以匹配完。在檢測的過程中,匹配上了,依次往fun字段寫入type中對應方法的地址。如果有一個方法沒有匹配上,那麼就設置fun[0]爲0,在外層調用會檢查fun[0]==0,即type並沒有實現interface。
這裏我們還可以看到golang中continue的特殊用法,要直接continue到外層的循環中,那麼就在那一層的循環上加個標籤,然後continue 標籤。
2.3.3 把itab插入到itabTable中 itabAdd
// itabAdd adds the given itab to the itab hash table.
// itabLock must be held.
func itabAdd(m *itab) {
// Bugs can lead to calling this while mallocing is set,
// typically because this is called while panicing.
// Crash reliably, rather than only when we need to grow
// the hash table.
if getg().m.mallocing != 0 {
throw("malloc deadlock")
}
t := itabTable
if t.count >= 3*(t.size/4) { // 75% load factor
// Grow hash table.
// t2 = new(itabTableType) + some additional entries
// We lie and tell malloc we want pointer-free memory because
// all the pointed-to values are not in the heap.
t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
t2.size = t.size * 2
// Copy over entries.
// Note: while copying, other threads may look for an itab and
// fail to find it. That's ok, they will then try to get the itab lock
// and as a consequence wait until this copying is complete.
iterate_itabs(t2.add)
if t2.count != t.count {
throw("mismatched count during itab table copy")
}
// Publish new hash table. Use an atomic write: see comment in getitab.
atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
// Adopt the new table as our own.
t = itabTable
// Note: the old table can be GC'ed here.
}
t.add(m)
}
// add adds the given itab to itab table t.
// itabLock must be held.
func (t *itabTableType) add(m *itab) {
// See comment in find about the probe sequence.
// Insert new itab in the first empty spot in the probe sequence.
mask := t.size - 1
h := itabHashFunc(m.inter, m._type) & mask
for i := uintptr(1); ; i++ {
p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
m2 := *p
if m2 == m {
// A given itab may be used in more than one module
// and thanks to the way global symbol resolution works, the
// pointed-to itab may already have been inserted into the
// global 'hash'.
return
}
if m2 == nil {
// Use atomic write here so if a reader sees m, it also
// sees the correctly initialized fields of m.
// NoWB is ok because m is not in heap memory.
// *p = m
atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
t.count++
return
}
h += I
h &= mask
}
}
可以看到,當hash表使用達到75%或以上時,就會進行擴容,容量是原來的2倍,申請完空間,就會把老表中的數據插入到新的hash表中。然後使itabTable指向新的表,最後把新的itab插入到新表中。
2.4 類型判斷
2.4.1 接口轉接口
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
tab := i.tab
if tab == nil {
return
}
if tab.inter != inter {
tab = getitab(inter, tab._type, true)
if tab == nil {
return
}
}
r.tab = tab
r.data = i.data
b = true
return
}
func assertE2I(inter *interfacetype, e eface) (r iface) {
t := e._type
if t == nil {
// explicit conversions require non-nil interface value.
panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
}
r.tab = getitab(inter, t, false)
r.data = e.data
return
}
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
t := e._type
if t == nil {
return
}
tab := getitab(inter, t, true)
if tab == nil {
return
}
r.tab = tab
r.data = e.data
b = true
return
}
首先我們看到有兩種用法:
- 返回值是一個時,不能轉換就panic。
- 返回值是兩個時,第二個返回值標記能否轉換成功
此外,data複製的是指針,不會完整拷貝值
2.4.2 接口轉具體類型
接口判斷是否轉換成具體類型,是編譯器生成好的代碼去做的。我們看個empty interface轉換成具體類型的例子:
var EFace interface{}
var j int
func F4(i int) int{
EFace = I
j = EFace.(int)
return j
}
func main() {
F4(10)
}
我們反彙編看一下
go build -gcflags '-N -l' -o tmp build.go
go tool objdump -s "main.F4" tmp
可以看到彙編代碼中有這麼一段,我加註釋你們就懂了:
MOVQ main.EFace(SB), CX //CX = EFace.typ
LEAQ type.*+60128(SB), DX //DX = &type.int
CMPQ DX, CX. //if DX == AX
可以看到empty interface轉具體類型,是編譯器生成好對比代碼,比較具體類型和空接口是不是同一個type,而不是調用某個函數在運行時動態對比。
然後我們再看下非空接口類型轉換:
var tf Tester
var t testStruct
func F4() int{
t := tf.(testStruct)
return t.i
}
func main() {
F4()
}
繼續反彙編看一下:
MOVQ main.tf(SB), CX // CX = tf.tab(.inter.typ)
LEAQ go.itab.main.testStruct,main.Tester(SB), DX // DX = <testStruct,Tester>對應的&itab(.inter.typ)
CMPQ DX, CX //
可以看到,非空接口轉具體類型,也是編譯器生成的代碼,比較是不是同一個itab,而不是調用某個函數在運行時動態對比。
最後
我還沒確定golang程序啓動時,是否會把編譯期生成的itab插入到全局的hash表中?
還有賦值給interface時,編譯優化避免malloc,這裏我也不太懂
誰知道可以告訴一下我。