背景
除了了與基礎(chǔ)性能息息相關(guān)的網(wǎng)絡(luò)和內(nèi)存管理之外纳胧,Golang 給人印象最深的一個(gè)特性就是 Inerface 數(shù)據(jù)結(jié)構(gòu)了,Interface 距離業(yè)務(wù)系統(tǒng)非常近郊丛,其獨(dú)特的靜態(tài)編譯,動(dòng)態(tài)檢測(cè)的類(lèi)型定義方式為提供了非常好的編程靈活性,大大簡(jiǎn)化了業(yè)務(wù)系統(tǒng)設(shè)計(jì)的復(fù)雜程度坊夫。
概述
通過(guò) Interface 你可以像使用Python、JavaScript這類(lèi)動(dòng)態(tài)類(lèi)型那樣的完成對(duì)象的類(lèi)型動(dòng)態(tài)轉(zhuǎn)換撤卢,與此同時(shí)作為一門(mén)傳統(tǒng)編譯型語(yǔ)言环凿,在編譯過(guò)程中,編譯器會(huì)幫你找到程序中類(lèi)型不匹配的問(wèn)題放吩。
下面我們嘗試通過(guò)一個(gè)簡(jiǎn)單的例子智听,看看 Interface 如何使用:
// 首先,聲明一個(gè)擁有兩個(gè)函數(shù)的接口類(lèi)型 ReadCloser渡紫,以及一個(gè)接收 ReadCloser 接口類(lèi)型參數(shù)的函數(shù)
type ReadCloser interface {
Read(b []byte) (n int, err os.Error)
Close()
}
func ReadAndClose(r ReadCloser, buf []byte) (n int, err os.Error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
r.Close()
return
}
// 接下來(lái)到推,然后聲明一個(gè)擁有一個(gè)函數(shù)的接口類(lèi)型 Stringer,以及一個(gè)接口 interface{} 接口類(lèi)型的函數(shù)
type Stringer interface {
String() string
}
func ToString(any interface{}) string {
if v, ok := any.(Stringer); ok {
return v.String()
}
switch v := any.(type) {
case int:
return strconv.Itoa(v)
case float:
return strconv.Ftoa(v, 'g', -1)
}
return "???"
}
// 最后是測(cè)試代碼
type stringer struct {
data string
}
func test1() {
stringer s
t := "hello world"
ReadAndClose(s, t)
}
func test2() {
stringer s
ToString(s)
}
函數(shù) test1 中由于我們的 stringer 數(shù)據(jù)結(jié)構(gòu)并沒(méi)有實(shí)現(xiàn) Read 和 Close 函數(shù)惕澎,此處會(huì)引起編譯時(shí)的報(bào)錯(cuò)莉测,而 test2 中由于使用 interface{} 編譯器不會(huì)它為綁定任何靜態(tài)類(lèi)型檢測(cè),因此編譯不會(huì)出錯(cuò)唧喉,函數(shù)體中第一句是一個(gè)類(lèi)型斷言捣卤,如果 any 對(duì)象可以轉(zhuǎn)換成 Stringer 接口類(lèi)忍抽,則 ok 為 true;如果不能完成轉(zhuǎn)換董朝,則 ok 為 false鸠项。如果類(lèi)型轉(zhuǎn)換成功,則調(diào)用 String 函數(shù)并返回結(jié)果子姜,如果轉(zhuǎn)換失敗祟绊,則做一個(gè)類(lèi)型判斷,判斷 any 的類(lèi)型是否是 int 或者 float闲询,如果是則調(diào)用 strconv 將數(shù)值轉(zhuǎn)換成字符串久免,如果不是則返回 "???"。
編譯器是如何判斷 any 對(duì)象是否可以完成類(lèi)型轉(zhuǎn)換呢扭弧?
編譯器通過(guò)檢查 any 所對(duì)應(yīng)的函數(shù)表中是否存在 String 這個(gè)函數(shù)阎姥,如果存在則可以完成類(lèi)型轉(zhuǎn)換,如果不存在則無(wú)法完成鸽捻。
PS: 需要說(shuō)明的是 "switch v := any.(type)" 一般也成為 type-switch呼巴,中文翻譯為類(lèi)型分支,可以算作做 type assertion 的語(yǔ)法糖御蒲,每個(gè)分支會(huì)被編譯器解釋為一句包含 type assertion 的 case 語(yǔ)句衣赶,示例如下:
package main
import (
"fmt"
"strconv"
)
type Stringer interface {
String() string
}
type Binary uint64
func (i Binary) String() string {
return strconv.Uitob64(i.Get(), 2)
}
func (i Binary) Get() uint64 {
return uint64(i)
}
func main() {
b := Binary(200)
s := Stringer(b)
fmt.Println(s.String())
}
使用 Golang 提供的編譯工具轉(zhuǎn)換成 Plan9 匯編,截取 test1 函數(shù)的代碼段
go tool compile -S main.go
main.test1 STEXT size=261 args=0x10 locals=0x50 funcid=0x0 align=0x0
+ 0x0000 00000 (main.go:7) TEXT main.test1(SB), ABIInternal, $80-16
+ 0x0000 00000 (main.go:7) CMPQ SP, 16(R14)
+ 0x0004 00004 (main.go:7) PCDATA $0, $-2
+ 0x0004 00004 (main.go:7) JLS 229
+ 0x000a 00010 (main.go:7) PCDATA $0, $-1
+ 0x000a 00010 (main.go:7) SUBQ $80, SP
+ 0x000e 00014 (main.go:7) MOVQ BP, 72(SP)
+ 0x0013 00019 (main.go:7) LEAQ 72(SP), BP
+ 0x0018 00024 (main.go:7) MOVQ AX, main.any+88(FP)
+ 0x001d 00029 (main.go:7) MOVQ BX, main.any+96(FP)
+ 0x0022 00034 (main.go:7) FUNCDATA $0, gclocals·IuErl7MOXaHVn7EZYWzfFA==(SB)
+ 0x0022 00034 (main.go:7) FUNCDATA $1, gclocals·EXTrhv4b3ahawRWAszmcVw==(SB)
+ 0x0022 00034 (main.go:7) FUNCDATA $2, main.test1.stkobj(SB)
+ 0x0022 00034 (main.go:7) FUNCDATA $5, main.test1.arginfo1(SB)
+ 0x0022 00034 (main.go:7) FUNCDATA $6, main.test1.argliveinfo(SB)
+ 0x0022 00034 (main.go:7) PCDATA $3, $1
+ 0x0022 00034 (main.go:8) TESTQ AX, AX
+ 0x0025 00037 (main.go:8) JEQ 219
+ 0x002b 00043 (main.go:8) MOVL 16(AX), DX
+ 0x002e 00046 (main.go:8) CMPL DX, $1810709754
+ 0x0034 00052 (main.go:8) JNE 137
++ 0x0036 00054 (main.go:9) LEAQ type.int32(SB), DX
+ 0x003d 00061 (main.go:9) NOP
+ 0x0040 00064 (main.go:9) CMPQ AX, DX
+ 0x0043 00067 (main.go:9) JNE 219
+ 0x0049 00073 (main.go:10) MOVUPS X15, main..autotmp_17+56(SP)
++ 0x004f 00079 (main.go:10) LEAQ type.string(SB), DX
+ 0x0056 00086 (main.go:10) MOVQ DX, main..autotmp_17+56(SP)
+ 0x005b 00091 (main.go:10) LEAQ main..stmp_0(SB), DX
+ 0x0062 00098 (main.go:10) MOVQ DX, main..autotmp_17+64(SP)
+ 0x0067 00103 (<unknown line number>) NOP
+ 0x0067 00103 ($GOROOT/src/fmt/print.go:294) MOVQ os.Stdout(SB), BX
+ 0x006e 00110 ($GOROOT/src/fmt/print.go:294) LEAQ go.itab.*os.File,io.Writer(SB), AX
+ 0x0075 00117 ($GOROOT/src/fmt/print.go:294) LEAQ main..autotmp_17+56(SP), CX
+ 0x007a 00122 ($GOROOT/src/fmt/print.go:294) MOVL $1, DI
+ 0x007f 00127 ($GOROOT/src/fmt/print.go:294) MOVQ DI, SI
+ 0x0082 00130 ($GOROOT/src/fmt/print.go:294) PCDATA $1, $1
+ 0x0082 00130 ($GOROOT/src/fmt/print.go:294) CALL fmt.Fprintln(SB)
+ 0x0087 00135 (main.go:8) JMP 219
+ 0x0089 00137 (main.go:8) CMPL DX, $-1920832363
+ 0x008f 00143 (main.go:8) JNE 219
++ 0x0091 00145 (main.go:11) LEAQ type.float32(SB), DX
+ 0x0098 00152 (main.go:11) CMPQ AX, DX
+ 0x009b 00155 (main.go:11) JNE 219
+ 0x009d 00157 (main.go:12) MOVUPS X15, main..autotmp_19+40(SP)
++ 0x00a3 00163 (main.go:12) LEAQ type.string(SB), DX
+ 0x00aa 00170 (main.go:12) MOVQ DX, main..autotmp_19+40(SP)
+ 0x00af 00175 (main.go:12) LEAQ main..stmp_1(SB), DX
+ 0x00b6 00182 (main.go:12) MOVQ DX, main..autotmp_19+48(SP)
+ 0x00bb 00187 (<unknown line number>) NOP
+ 0x00bb 00187 ($GOROOT/src/fmt/print.go:294) MOVQ os.Stdout(SB), BX
+ 0x00c2 00194 ($GOROOT/src/fmt/print.go:294) LEAQ go.itab.*os.File,io.Writer(SB), AX
+ 0x00c9 00201 ($GOROOT/src/fmt/print.go:294) LEAQ main..autotmp_19+40(SP), CX
+ 0x00ce 00206 ($GOROOT/src/fmt/print.go:294) MOVL $1, DI
+ 0x00d3 00211 ($GOROOT/src/fmt/print.go:294) MOVQ DI, SI
+ 0x00d6 00214 ($GOROOT/src/fmt/print.go:294) CALL fmt.Fprintln(SB)
+ 0x00db 00219 (main.go:14) PCDATA $1, $-1
+ 0x00db 00219 (main.go:14) MOVQ 72(SP), BP
+ 0x00e0 00224 (main.go:14) ADDQ $80, SP
+ 0x00e4 00228 (main.go:14) RET
+ 0x00e5 00229 (main.go:14) NOP
+ 0x00e5 00229 (main.go:7) PCDATA $1, $-1
+ 0x00e5 00229 (main.go:7) PCDATA $0, $-2
+ 0x00e5 00229 (main.go:7) MOVQ AX, 8(SP)
+ 0x00ea 00234 (main.go:7) MOVQ BX, 16(SP)
+ 0x00ef 00239 (main.go:7) CALL runtime.morestack_noctxt(SB)
+ 0x00f4 00244 (main.go:7) MOVQ 8(SP), AX
+ 0x00f9 00249 (main.go:7) MOVQ 16(SP), BX
+ 0x00fe 00254 (main.go:7) PCDATA $0, $-1
+ 0x00fe 00254 (main.go:7) NOP
+ 0x0100 00256 (main.go:7) JMP 0
// 省略后面的非函數(shù)聲明部分的源碼
// ...
可以看到四行 type assertion 被標(biāo)記出來(lái)了(行首標(biāo)記為++)厚满。
回到類(lèi)型斷言的問(wèn)題來(lái)府瞄,接下來(lái)看一個(gè)簡(jiǎn)單的例子。
type Binary uint64
var _ Stringer = (*Binary)(nil)
func (i Binary) String() string {
return strconv.Uitob64(i.Get(), 2)
}
func (i Binary) Get() uint64 {
return uint64(i)
}
如果我們定義一個(gè) Binary 類(lèi)型的變量將其傳入 ToString() 函數(shù)時(shí)碘箍,由于我們?yōu)?Binary 類(lèi)型定義了 String() 函數(shù)遵馆,因此 any.(Stringer) 可以將 Binary 轉(zhuǎn)換成 Stringer 接口類(lèi)型。這就是我們說(shuō)的 Golang 接口可以實(shí)現(xiàn) ”鴨子類(lèi)型“ 的威力丰榴,我們無(wú)需顯示的聲明接口類(lèi)型货邓,編譯器會(huì)通過(guò)接口比對(duì)的方式為我們校驗(yàn)類(lèi)型是否匹配。需要主要是工程上四濒,Golang 鴨子類(lèi)型雖然有靈活性的優(yōu)點(diǎn)换况,但是一般跨包或是項(xiàng)目去實(shí)現(xiàn)某個(gè)接口時(shí),程序會(huì)變的可讀性非常差并且極容器出錯(cuò)盗蟆,那我們能不能像 Java 中的接口一樣戈二,讓我們知道一個(gè)接口中我們需要實(shí)現(xiàn)具體定義哪些函數(shù),類(lèi)似于”接口繼承”的語(yǔ)法喳资。
Interface 實(shí)現(xiàn)
Golang 的類(lèi)型設(shè)計(jì)原則中挽拂,一般包含 type 和 value 兩部分, Interface 的實(shí)現(xiàn)也遵循這個(gè)原則骨饿,不過(guò)亏栈,golang 編譯器會(huì)根據(jù) interface 是否包含有 method,實(shí)現(xiàn)上用兩種不同數(shù)據(jù)結(jié)構(gòu)來(lái):一種是有 method 的 interface 對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)為 iface宏赘;一種是沒(méi)有 method 的 empty interface 對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)為 eface绒北。
// eface 數(shù)據(jù)結(jié)構(gòu)
type eface struct {
_type *_type // 類(lèi)型信息
data unsafe.Pointer // 原數(shù)據(jù)存放的位置
}
// 大多數(shù)的 Golang 中的數(shù)據(jù)結(jié)構(gòu)其底層都會(huì)對(duì)應(yīng)一種 _type 類(lèi)型的數(shù)據(jù)結(jié)構(gòu)
type _type struct {
size uintptr // type size
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldalign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
alg *typeAlg // algorithm table
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
// iface 數(shù)據(jù)結(jié)構(gòu)
type iface struct {
tab *itab // 可以理解為含有接口函數(shù)表的類(lèi)型信息
data unsafe.Pointer // 原數(shù)據(jù)存放的位置
}
type itab struct {
inter *interfacetype // interface 類(lèi)型信息
_type *_type // 原數(shù)據(jù)結(jié)構(gòu)的類(lèi)型信息
link *itab
bad int32
inhash int32 // 只有 itab 被擁有記錄在了 hash 表中
fun [1]uintptr // 函數(shù)表入口指針
}
// 函數(shù)名聲明
type imethod struct {
name nameOff
ityp typeOff
}
type interfacetype struct {
typ _type // interface type
pkgpath name // 包路徑
mhdr []imethod // 接口函數(shù)名聲明表
}
需要說(shuō)明:
- 對(duì)于非空的接口,其對(duì)應(yīng)的類(lèi)型有兩個(gè)察署,一般稱(chēng)為 interface type 和 concrete type闷游, interface type 保存在 itab.inter.typ 中,concrete type 保存在 itab._type 中贴汪。
- itab 中存放了兩張函數(shù)表脐往,一張表對(duì)應(yīng)接口對(duì)應(yīng)實(shí)際接收到的函數(shù),通過(guò) itab.fun[0] 指向第一個(gè)函數(shù)對(duì)應(yīng)的函數(shù)指針扳埂,后續(xù)函數(shù)根據(jù)函數(shù)名的字典值有小到大的排列业簿,與C++中對(duì)象的虛函數(shù)表及虛函數(shù)的定義方式非常類(lèi)似,另外一張表對(duì)應(yīng)接口定制是函數(shù)聲明阳懂。
下面通過(guò)一段代碼樣例梅尤,展示 Golang 如何實(shí)現(xiàn) interface 對(duì)象的創(chuàng)建及類(lèi)型轉(zhuǎn)換。
package main
import (
"fmt"
"strconv"
)
type Stringer interface {
String() string
}
type Binary uint64
func (i Binary) String() string {
return strconv.Uitob64(i.Get(), 2)
}
func (i Binary) Get() uint64 {
return uint64(i)
}
func main() {
1 b := Binary(200)
2 var a interface{} = b
3 s := Stringer(b)
4 fmt.Println(s.String())
}
main 函數(shù)代碼第一行岩调,由于 Binary 沒(méi)有實(shí)現(xiàn)任何函數(shù)時(shí)巷燥,因此 對(duì)象 b 只是一個(gè)普通的數(shù)據(jù)結(jié)構(gòu),其對(duì)應(yīng)的內(nèi)存區(qū)域存放對(duì)應(yīng)的 uint64 數(shù)值号枕。
代碼第二行缰揪,嘗試創(chuàng)建一個(gè)空接口對(duì)象,然后將 Binary 對(duì)象賦值給它葱淳,編譯器會(huì)構(gòu)造一個(gè) eface 類(lèi)型的數(shù)據(jù)結(jié)構(gòu)钝腺,然后將 Binary 對(duì)應(yīng)的 _type 以及 Binary 對(duì)應(yīng)的數(shù)據(jù)保存下來(lái)。
代碼第三行蛙紫,創(chuàng)建一個(gè) Stringer 類(lèi)型的接口對(duì)象拍屑,然后將 Binary 對(duì)象賦值給它,編譯器會(huì)構(gòu)造一個(gè) iface 類(lèi)型的數(shù)據(jù)結(jié)構(gòu)坑傅,itab中分別保存 Binary 和 Stringer 對(duì)應(yīng)的 _type 以及 定義了 Binary 作為接收器的函數(shù)指針僵驰。
類(lèi)型斷言(Type Assertion)
根據(jù)前面看過(guò)了的例子,我們知道類(lèi)型斷言的語(yǔ)句會(huì)被替換成 runtime 包中的 assert 函數(shù)唁毒,那我們把這兩個(gè)函數(shù)的源碼貼出來(lái)蒜茴,需要說(shuō)明一下 assertI2I 是 iface 類(lèi)型的接口類(lèi)型斷言對(duì)應(yīng)的函數(shù),assertE2I 是 eface 類(lèi)型的接口類(lèi)型斷言對(duì)應(yīng)的函數(shù)浆西。
類(lèi)型斷言的工作其實(shí)非常簡(jiǎn)單粉私,先判斷 itab.inter 這個(gè)接口類(lèi)型是否相同,如果相同直接返回近零,如果不同則進(jìn)入 getitab 進(jìn)行處理诺核,重點(diǎn)看一下這個(gè)函數(shù)抄肖。
func assertI2I(inter *interfacetype, tab *itab) *itab {
if tab == nil {
// explicit conversions require non-nil interface value.
panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
}
if tab.inter == inter {
return tab
}
//
return getitab(inter, tab._type, false)
}
func assertI2I2(inter *interfacetype, i iface) (r iface) {
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
return
}
func assertE2I(inter *interfacetype, t *_type) *itab {
if t == nil {
// explicit conversions require non-nil interface value.
panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
}
return getitab(inter, t, false)
}
func assertE2I2(inter *interfacetype, e eface) (r iface) {
t := e._type
if t == nil {
return
}
tab := getitab(inter, t, true)
if tab == nil {
return
}
r.tab = tab
r.data = e.data
return
}
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
// _, ok := a.(interface{}) 這樣的空接口斷言直接拋出錯(cuò)誤。
if len(inter.mhdr) == 0 {
throw("internal error - misuse of itab")
}
// 判斷傳入類(lèi)型是否為 Uncomon type
// Golang 類(lèi)型定義這里就不展開(kāi)做詳細(xì)的講解了窖杀,Uncommon Type可以簡(jiǎn)單理解為一個(gè)綁定了 Methods 的數(shù)據(jù)結(jié)構(gòu)漓摩。
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
// itabTable 是編譯器創(chuàng)建的 itab 緩存哈希表,先通過(guò)原子操作查表找到 itab 如果沒(méi)有找到入客,則再通過(guò)加鎖方式查找一遍
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
}
// 經(jīng)過(guò)兩次查表都沒(méi)有找到 itab 對(duì)應(yīng)的類(lèi)型管毙,就創(chuàng)建一個(gè)新的 itab 對(duì)象,并將其存入 itabTable 中
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*goarch.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
m.hash = 0
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()})
}
更多技術(shù)分享瀏覽我的博客:
參考
本文由mdnice多平臺(tái)發(fā)布