本文介紹了Go語言中接口(interface)的內部實現、nil interface和nil的區別以及使用時的一些坑。
上篇文章回顧: Elasticsearch SQL用法詳解
接口(interface)表明一種「約定」或「協議」,是多個方法聲明的集合。容許在非顯示關聯狀況下,組合並調用其它類型的方法。接口無需依賴類型,帶來的優勢就是減小調用者可視化方法,隱藏類型內部結構和具體方法實現細節。雖然接口的優勢有不少,可是接口的實現是在運行期實現的,因此存在其它額外的開銷。在平常開發過程當中是否選擇接口須要根據場景進行合理的選擇。
bash
一個接口須要包括方法簽名,方法簽名包括:方法名稱、參數和返回列表。接口內不能有字段,並且不能定義本身的方法,這主要是因爲字段和方法的定義須要分配內存。運維
package main
import (
"fmt"
"reflect"
)
type Ser interfacee {
A(a int)
B()
}
type X int
func (X) A(b int) {}
func (*X) B() {}
var o X
var _ Ser = &o
func main() {}複製代碼
Go語言接口是隱式實現的,這意味着開發人員不須要聲明它實現的接口。雖然這一般很是方便,但在某些狀況下可能須要明確檢查接口的實現。最好的方法就是依賴編譯器實現,例如:函數
package main
type Jedi interface {
HasForce() bool
}
type Knight struct {}
var _ Jedi = (*Knight)(nil) // 利用編譯器檢查接口實現
func main() {}複製代碼
接口調用是經過所屬於它的方法集進行調用,而類型調用則經過它所屬於的方法進行調用,它們之間有本質的差異。接下來講說接口是如何實現的,以及如何獲取接口的方法集。性能
runtime中有兩種方式對接口實現,一種是iface類型,另外一種是eface。優化
// 接口內包含有方法的實現
type iface struct {
tab *itab
data unsafe.Pointer // 實際對象指針
}
// 類型信息
type itab struct {
inter *interfacetype // 接口類型
_type *_type // 實際類型對象
fun [1]uintptr // 實際對象方法地址
}
// 接口內不包含方法的實現,即nil interface.
type eface struct {
_type *_type
data unsafe.Pointer
}複製代碼
2.2.1 按值實現接口ui
type T struct {}
type Ter interface{
A()
B()
}
func(t T) A(){}
func(t *T) B(){}
var o T
var i Ter = o複製代碼
當將o實現接口Ter時,實際上是將T類型內存拷貝一份,而後i.data指向新生成複製品的內存地址。當調用i.A()方法時,通過如下3個步驟:this
1. 經過i.(*data)變量獲取複製品內的內容。spa
2. 獲取i.(*data).A內存。3d
3. 調用i.(*data).A()方法。指針
當調用i.B()方法時,因爲receiver的是*T.B()和T.A()是不同的,調用通過也存在區別:
1. 經過i.(*data)變量獲取其內容(此時的內容指向類型T的指針)。
2. 因爲i.(*data)變量獲取的內容是地址,因此須要進行取地址操做。但Go內部實現禁止對該複製品進行取地址操做,因此沒法調用i.B()方法。
因此代碼進行編譯時會報錯:
T does not implement Ter (B method has pointer receiver)
2.2.2 按指針實現接口
對以上代碼進行稍加改動:
var o T
var i Ter = &o複製代碼
此時經過調用i.A()和i.B()方法時是如何實現的呢?
1. 經過i.(*data)變量獲取複製品內容(此時內容爲指向類型T的指針)。
2. 獲取複製品內容(即T類型地址),而後調用類型T的A和B方法。
2.2.3 接口方法集合
經過以上對接口實現分析,能夠得出接口的方法集是:
1. 類型T的方法集包含全部receiver T方法。
2. 類型*T的方法集合包含全部Receiver T + *T方法。
nil interface和nil有什麼區別呢?我們能夠經過兩個demo來看看它們具體有什麼區別。
接口內部tab和data均爲空時,接口才爲nil。
// go:noinline
func main() {
var i interface{}
if i == nil {
println(「The interface is nil.「)
}
}
(gdb) info locals;
i = {_type = 0x0, data = 0x0}
(gdb) ptype i
type = struct runtime.eface {
runtime._type *_type;
void *data;
}複製代碼
若是接口內部data值爲nil,但tab不爲空時,此時接口爲nil interface。
// go:noinline
func main() {
var o *int = nil
var i interface{} = o
if i == nil {
println("Nil")
}
println(i)
}
(gdb) info locals;
i = {_type = 0x1050fe0 <type.*+25568>, data = 0x0}
o = 0x0
(gdb) ptype i
type = struct runtime.eface {
runtime._type *_type;
void *data;
}複製代碼
能夠利用reflect(反射)進行nil檢查:
fun main() {
var o *int = nil
var a interface{} = o
var b interface{}
println(a == nil, b == nil) // false, true
v := reflect.ValueOf(a)
if v.Isvalid() {
println(v.IsNil()) // true, This is nil interface
}
}
(gdb) ptype v
type = struct reflect.Value {
struct reflect.rtype *typ;
void *ptr;
reflect.flag flag;
}複製代碼
固然也能夠經過unsafe進行檢查:
v := reflet.ValueOf(a)
*(*unsae.Pointer)(v.ptr) == nil複製代碼
在文章剛開始就已經介紹了接口有不少優勢,因爲接口是在運行期實現的,因此它採用動態方法調用。相比類型直接(或靜態)方法調用,性能確定有消耗,可是這種性能的消耗不大,而主要影響是對象逃逸和沒法內聯。
實例1:
package main
type T struct{}
func (t *T) A() {}
func (t *T) B() {}
type Ter interface{
A()
B()
}
func main() {
var t T
var ter Ter = &t
ter.A()
ter.B()
}複製代碼
反彙編:
TEXT main.main(SB) /Users/David/data/go/go.test/src/Demo/main.go
main.go:21 0x104ab90 65488b0c25a0080000 MOVQ GS:0x8a0, CX
main.go:21 0x104ab99 483b6110 CMPQ 0x10(CX), SP
main.go:21 0x104ab9d 7652 JBE 0x104abf1
main.go:21 0x104ab9f 4883ec20 SUBQ $0x20, SP
main.go:21 0x104aba3 48896c2418 MOVQ BP, 0x18(SP)
main.go:21 0x104aba8 488d6c2418 LEAQ 0x18(SP), BP
main.go:22 0x104abad 488d054cd80000 LEAQ runtime.rodata+55200(SB), AX
main.go:22 0x104abb4 48890424 MOVQ AX, 0(SP)
main.go:22 0x104abb8 e86303fcff CALL runtime.newobject(SB)
main.go:27 0x104abbd 488d059c710200 LEAQ go.itab.*main.T,main.Ter(SB), AX
main.go:27 0x104abc4 8400 TESTB AL, 0(AX)
main.go:22 0x104abc6 488b442408 MOVQ 0x8(SP), AX
main.go:22 0x104abcb 4889442410 MOVQ AX, 0x10(SP)
main.go:27 0x104abd0 48890424 MOVQ AX, 0(SP)
main.go:27 0x104abd4 e8f7feffff CALL main.(*T).A(SB)
main.go:27 0x104abd9 488b442410 MOVQ 0x10(SP), AX
main.go:28 0x104abde 48890424 MOVQ AX, 0(SP)
main.go:28 0x104abe2 e849ffffff CALL main.(*T).B(SB)
main.go:29 0x104abe7 488b6c2418 MOVQ 0x18(SP), BP
main.go:29 0x104abec 4883c420 ADDQ $0x20, SP
main.go:29 0x104abf0 c3 RET
main.go:21 0x104abf1 e82a88ffff CALL runtime.morestack_noctxt(SB)
main.go:21 0x104abf6 eb98 JMP main.main(SB)
:-1 0x104abf8 cc INT $0x3
:-1 0x104abf9 cc INT $0x3
:-1 0x104abfa cc INT $0x3
:-1 0x104abfb cc INT $0x3
:-1 0x104abfc cc INT $0x3
:-1 0x104abfd cc INT $0x3
:-1 0x104abfe cc INT $0x3
:-1 0x104abff cc INT $0x3複製代碼
經過以上反彙編代碼能夠看到接口調用方法是經過動態調用方式進行調用。
實例2:
package main
type T struct{}
func (t *T) A() {
println("A")
}
func (t *T) B() {
println("B")
}
type Ter interface{
A()
B()
}
func main() {
var t T
t.A()
t.B()
}複製代碼
以上代碼在函數A和B內輸出print,主要防止被內聯以後,在main函數看不到效果。
反彙編:
TEXT main.main(SB) /Users/David/data/go/go.test/src/Demo/main.go
main.go:21 0x104aad0 65488b0c25a0080000 MOVQ GS:0x8a0, CX
main.go:21 0x104aad9 483b6110 CMPQ 0x10(CX), SP
main.go:21 0x104aadd 765e JBE 0x104ab3d
main.go:21 0x104aadf 4883ec18 SUBQ $0x18, SP
main.go:21 0x104aae3 48896c2410 MOVQ BP, 0x10(SP)
main.go:21 0x104aae8 488d6c2410 LEAQ 0x10(SP), BP
main.go:9 0x104aaed e8de6afdff CALL runtime.printlock(SB)
main.go:9 0x104aaf2 488d055bbf0100 LEAQ go.string.*+36(SB), AX
main.go:9 0x104aaf9 48890424 MOVQ AX, 0(SP)
main.go:9 0x104aafd 48c744240802000000 MOVQ $0x2, 0x8(SP)
main.go:9 0x104ab06 e80574fdff CALL runtime.printstring(SB)
main.go:9 0x104ab0b e8406bfdff CALL runtime.printunlock(SB)
main.go:13 0x104ab10 e8bb6afdff CALL runtime.printlock(SB)
main.go:13 0x104ab15 488d053abf0100 LEAQ go.string.*+38(SB), AX
main.go:13 0x104ab1c 48890424 MOVQ AX, 0(SP)
main.go:13 0x104ab20 48c744240802000000 MOVQ $0x2, 0x8(SP)
main.go:13 0x104ab29 e8e273fdff CALL runtime.printstring(SB)
main.go:13 0x104ab2e e81d6bfdff CALL runtime.printunlock(SB)
main.go:13 0x104ab33 488b6c2410 MOVQ 0x10(SP), BP
main.go:13 0x104ab38 4883c418 ADDQ $0x18, SP
main.go:13 0x104ab3c c3 RET
main.go:21 0x104ab3d e8de88ffff CALL runtime.morestack_noctxt(SB)
main.go:21 0x104ab42 eb8c JMP main.main(SB)
:-1 0x104ab44 cc INT $0x3
:-1 0x104ab45 cc INT $0x3
:-1 0x104ab46 cc INT $0x3
:-1 0x104ab47 cc INT $0x3
:-1 0x104ab48 cc INT $0x3
:-1 0x104ab49 cc INT $0x3
:-1 0x104ab4a cc INT $0x3
:-1 0x104ab4b cc INT $0x3
:-1 0x104ab4c cc INT $0x3
:-1 0x104ab4d cc INT $0x3
:-1 0x104ab4e cc INT $0x3
:-1 0x104ab4f cc INT $0x3複製代碼
經過使用接口和類型兩種方式發現,接口採用動態方法調用而類型方法調用被編譯器直接內聯了(直接將方法調用展開在了方法調用處,減小了內存調用stack開銷)。因此採用類型直接方法調用性能優於使用接口調用。
如今觀察如下經過類型直接方法調用和經過接口動態方法調用編譯器如何進行優化。
4.2.1 編譯器對類型方法優化
# Demo
./main.go:8:6: can inline (*T).A
./main.go:12:6: can inline (*T).B
./main.go:21:6: can inline main
./main.go:23:8: inlining call to (*T).A
./main.go:24:8: inlining call to (*T).B
./main.go:8:10: (*T).A t does not escape
./main.go:12:10: (*T).B t does not escape
./main.go:23:6: main t does not escape
./main.go:24:6: main t does not escape
<autogenerated>:1:0: leaking param: .this
<autogenerated>:1:0: leaking param: .this複製代碼
4.2.2 編譯器對接口方法優化
# Demo
./main.go:8:6: can inline (*T).A
./main.go:12:6: can inline (*T).B
./main.go:8:10: (*T).A t does not escape
./main.go:12:10: (*T).B t does not escape
./main.go:26:9: &t escapes to heap
./main.go:26:19: &t escapes to heap
./main.go:22:9: moved to heap: t
<autogenerated>:1:0: leaking param: .this
<autogenerated>:1:0: leaking param: .this複製代碼
經過編譯器對程序優化輸出得出,當使用接口方式進行方法調用時main函數內的&t發生了逃逸。
今天僅對接口的具體實現進行了簡單分析,接口有它的優點同時也有它的缺點。在平常工程開發過程當中如何選擇仍是須要根據具體的場景進行具體分析。但願本篇文章對你們有所幫助。
本文首發於公衆號「小米運維」,點擊查看原文