Go interface實現分析

小米運維發表於2018-12-18
本文介紹了Go語言中介面(interface)的內部實現、nil interface和nil的區別以及使用時的一些坑。
上篇文章回顧:Elasticsearch SQL用法詳解


前言

介面(interface)代表一種“約定”或“協議”,是多個方法宣告的集合。允許在非顯示關聯情況下,組合並呼叫其它型別的方法。介面無需依賴型別,帶來的優點就是減少呼叫者視覺化方法,隱藏型別內部結構和具體方法實現細節。雖然介面的優點有很多,但是介面的實現是在執行期實現的,所以存在其它額外的開銷。在日常開發過程中是否選擇介面需要根據場景進行合理的選擇。

1、介面定義

一個介面需要包括方法簽名,方法簽名包括:方法名稱、引數和返回列表。介面內不能有欄位,而且不能定義自己的方法,這主要是由於欄位和方法的定義需要分配記憶體。

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() {}複製程式碼

>>>>1.1 如何確保型別實現介面

Go語言介面是隱式實現的,這意味著開發人員不需要宣告它實現的介面。雖然這通常非常方便,但在某些情況下可能需要明確檢查介面的實現。最好的方法就是依賴編譯器實現,例如:

package main

type Jedi interface {
    HasForce() bool
}

type Knight struct {}

var _ Jedi = (*Knight)(nil)       // 利用編譯器檢查介面實現

func main() {}複製程式碼

2、介面內部實現

介面呼叫是通過所屬於它的方法集進行呼叫,而型別呼叫則通過它所屬於的方法進行呼叫,它們之間有本質的差別。接下來說說介面是如何實現的,以及如何獲取介面的方法集。

>>>>2.1 介面內部實現

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 按值實現介面和按指標實現介面區別

2.2.1 按值實現介面

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個步驟:

1. 通過i.(*data)變數獲取複製品內的內容。

2. 獲取i.(*data).A記憶體。

3. 呼叫i.(*data).A()方法。

Go interface實現分析

當呼叫i.B()方法時,由於receiver的是*T.B()和T.A()是不一樣的,呼叫經過也存在區別:

1. 通過i.(*data)變數獲取其內容(此時的內容指向型別T的指標)。

2. 由於i.(*data)變數獲取的內容是地址,所以需要進行取地址操作。但Go內部實現禁止對該複製品進行取地址操作,所以無法呼叫i.B()方法。

Go interface實現分析

所以程式碼進行編譯時會報錯:

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方法。

3、nil interface和nil區別

nil interface和nil有什麼區別呢?我們們可以通過兩個demo來看看它們具體有什麼區別。

>>>>3.1 nil

介面內部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;
}複製程式碼

>>>>3.2 nil interface

如果介面內部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;
}複製程式碼

>>>>3.3 介面nil檢查

可以利用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複製程式碼

4、interface效能問題

在文章剛開始就已經介紹了介面有很多優點,由於介面是在執行期實現的,所以它採用動態方法呼叫。相比型別直接(或靜態)方法呼叫,效能肯定有消耗,但是這種效能的消耗不大,而主要影響是物件逃逸和無法內聯。

>>>>4.1 介面動態呼叫對效能影響

例項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 記憶體逃逸

現在觀察以下通過型別直接方法呼叫和通過介面動態方法呼叫編譯器如何進行優化。

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發生了逃逸。

5、總結

今天僅對介面的具體實現進行了簡單分析,介面有它的優勢同時也有它的缺點。在日常工程開發過程中如何選擇還是需要根據具體的場景進行具體分析。希望本篇文章對大家有所幫助。


本文首發於公眾號“小米運維”,點選檢視原文


相關文章