具體類型轉換成接口類型
到此已經知道什麼是接口以及接口的底層結構,那麼當具體類型賦值給接口類型時,是如何進行轉換的?再來看下2.3中的示例
package main
import "fmt"
type Runner interface {
Run()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func main() {
var r Runner
r = Person{Name: "song_chh"}
r.Run()
}
通過Go提供的工具生成彙編代碼
go tool compile -S interface.go
只截取與第19行相關的代碼
0x001d 00029 (interface.go:19) PCDATA $2, $0
0x001d 00029 (interface.go:19) PCDATA $0, $1
0x001d 00029 (interface.go:19) XORPS X0, X0
0x0020 00032 (interface.go:19) MOVUPS X0, ""..autotmp_1+32(SP)
0x0025 00037 (interface.go:19) PCDATA $2, $1
0x0025 00037 (interface.go:19) LEAQ go.string."song_chh"(SB), AX
0x002c 00044 (interface.go:19) PCDATA $2, $0
0x002c 00044 (interface.go:19) MOVQ AX, ""..autotmp_1+32(SP)
0x0031 00049 (interface.go:19) MOVQ $8, ""..autotmp_1+40(SP)
0x003a 00058 (interface.go:19) PCDATA $2, $1
0x003a 00058 (interface.go:19) LEAQ go.itab."".Person,"".Runner(SB), AX
0x0041 00065 (interface.go:19) PCDATA $2, $0
0x0041 00065 (interface.go:19) MOVQ AX, (SP)
0x0045 00069 (interface.go:19) PCDATA $2, $1
0x0045 00069 (interface.go:19) PCDATA $0, $0
0x0045 00069 (interface.go:19) LEAQ ""..autotmp_1+32(SP), AX
0x004a 00074 (interface.go:19) PCDATA $2, $0
0x004a 00074 (interface.go:19) MOVQ AX, 8(SP)
0x004f 00079 (interface.go:19) CALL runtime.convT2I(SB)
0x0054 00084 (interface.go:19) MOVQ 16(SP), AX
0x0059 00089 (interface.go:19) PCDATA $2, $2
0x0059 00089 (interface.go:19) MOVQ 24(SP), CX
可以看到,編譯器在構造itab後調用runtime.convT2I(SB)轉換函數,看下函數的實現
//runtime/iface.go
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
}
首先根據類型大小調用mallocgc申請一塊內存空間,將elem指針的內容拷貝到新空間,將tab賦值給iface的tab,將新內存指針賦值給iface的data,這樣一個iface就創建完成
將示例代碼稍作更改,使用結構體指針類型的變量賦值給接口變量
r = &Person{Name: "song_chh"}
再次通過工具生成彙編代碼
go tool compile -S interface.go
0x001d 00029 (interface.go:19) PCDATA $2, $1
0x001d 00029 (interface.go:19) PCDATA $0, $0
0x001d 00029 (interface.go:19) LEAQ type."".Person(SB), AX
0x0024 00036 (interface.go:19) PCDATA $2, $0
0x0024 00036 (interface.go:19) MOVQ AX, (SP)
0x0028 00040 (interface.go:19) CALL runtime.newobject(SB)
0x002d 00045 (interface.go:19) PCDATA $2, $2
0x002d 00045 (interface.go:19) MOVQ 8(SP), DI
0x0032 00050 (interface.go:19) MOVQ $8, 8(DI)
0x003a 00058 (interface.go:19) PCDATA $2, $-2
0x003a 00058 (interface.go:19) PCDATA $0, $-2
0x003a 00058 (interface.go:19) CMPL runtime.writeBarrier(SB), $0
0x0041 00065 (interface.go:19) JNE 105
0x0043 00067 (interface.go:19) LEAQ go.string."song_chh"(SB), AX
0x004a 00074 (interface.go:19) MOVQ AX, (DI)
首先編譯器通過type."".Person(SB)獲取Person結構體類型,作爲參數調用runtime.newobject()函數,同樣的在源碼中查看函數定義
import "unsafe"
// runtime/malloc.go
// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
newobject以*Person作爲入參,創建新的Person結構體指針,之後由編譯器設置值,iface由編譯器直接生成
除了convT2I函數外,其實在runtime/runtime.go文件中,還有很多轉換函數的定義
// Non-empty-interface to non-empty-interface conversion.
func convI2I(typ *byte, elem any) (ret any)
// Specialized type-to-interface conversion.
// These return only a data pointer.
func convT16(val any) unsafe.Pointer // val must be uint16-like (same size and alignment as a uint16)
func convT32(val any) unsafe.Pointer // val must be uint32-like (same size and alignment as a uint32)
func convT64(val any) unsafe.Pointer // val must be uint64-like (same size and alignment as a uint64 and contains no pointers)
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer // val must be a slice
// Type to empty-interface conversion.
func convT2E(typ *byte, elem *any) (ret any)
func convT2Enoptr(typ *byte, elem *any) (ret any)
// Type to non-empty-interface conversion.
func convT2I(tab *byte, elem *any) (ret any) //for the general case
func convT2Inoptr(tab *byte, elem *any) (ret any) //for structs that do not contain pointers
convI2I用於接口轉換成另一個接口時調用,在3.4會進行講解
convT2Inoptr用於變量內部不含指針的轉換,noptr可以理解爲no pointer,轉換過程與convT2I類似
convT16、convT32、convT64、convTstring 和 convTslice是針對簡單類型轉接口的特例優化,有興趣的可以看下函數實現的源碼,因爲這幾個函數內容相似,這裏就簡單介紹下convT64
//runtime/iface.go
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
}
相較於convT2系列函數,缺少typedmemmove和memmove函數的調用,減少內存拷貝。另外如果變量值爲該類型的零值,則不會調用 mallocgc 去申請一塊新內存,而是直接返回指向zeroVal[0]的指針
再來看下,空接口轉換函數convT2E
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
}
接口與接口的轉換
如果某個類型實現多個接口,那接口直接是如何進行轉換的,還是先看一段示例:
package main
import "fmt"
type Runner interface {
Run()
Say()
}
type Sayer interface {
Say()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func (p Person) Say() {
fmt.Printf("hello, %s", p.Name)
}
func main() {
var r Runner
r = Person{Name: "song_chh"}
var s Sayer
s = r
s.Say()
}
增加Sayer接口定義,包含Say()方法,在main函數中聲明一個Sayer變量,並將Runner接口變量賦值給Sayer變量。因爲Person實現了Say()方法,所以說Person既實現了是Runner接口,又實現了Sayer接口
執行命令
go tool compile -S interface.go
截取32行彙編代碼
0x0062 00098 (interface.go:32) PCDATA $2, $3
0x0062 00098 (interface.go:32) LEAQ type."".Sayer(SB), DX
0x0069 00105 (interface.go:32) PCDATA $2, $2
0x0069 00105 (interface.go:32) MOVQ DX, (SP)
0x006d 00109 (interface.go:32) MOVQ AX, 8(SP)
0x0072 00114 (interface.go:32) PCDATA $2, $0
0x0072 00114 (interface.go:32) MOVQ CX, 16(SP)
0x0077 00119 (interface.go:32) CALL runtime.convI2I(SB)
0x007c 00124 (interface.go:32) MOVQ 24(SP), AX
0x0081 00129 (interface.go:32) PCDATA $2, $2
0x0081 00129 (interface.go:32) MOVQ 32(SP), CX
可以看到在執行期間,調用runtime.convI2I進行接口轉換,接下來看下源代碼
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 = (inter, tab._type, false)
r.data = i.data
return
}
函數參數inter表示接口的類型,由編譯器生成,即type."".Sayer(SB),i 是綁定實體的接口, r 是轉換後新的接口,如果要轉換的接口是同一類型,則直接把 i 的tab和data給新接口 r ,將 r 返回。如果要轉換的接口不是同一類型,則通過getitab生成一個新的tab複製給r.tab,然後將 r 返回
那麼具體來看一下getitab這個函數,還是先看源碼
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.find進行查找
再沒有找到,就會根據具體類型typ和接口類型inter生成一個itab,並將這個新生成的itab添加到全局的itabTable中。如果具體類型並沒有實現接口,根據canfail值返回nil或者painc