我可能並不會使用golang interface

談到interface,咱們大體應該會有這樣的疑問linux

  • interface是什麼?
  • 他跟面嚮對象語言中的接口有啥區別?
  • 他的底層原理是什麼樣的?
  • interface的優缺點是什麼?
  • interface有哪些常見的特殊狀況和使用技巧?

上述大概涵蓋了,咱們的主要的疑問,有問題是好事兒,咱們慢慢來看看。git

1.什麼是interface

在Go中,接口是一組方法簽名。 當類型爲接口中的全部方法提供定義時,就說實現了該接口。 它與OOP世界很是類似。 接口指定類型應具備的方法,類型決定如何實現這些方法。github

例如,WashingMachine能夠是具備方法簽名Cleaning()Drying()的接口。 任何提供Cleaning()Drying()方法定義的類型均可以說是實現了WashingMachine接口。數組

2. 和其餘語言中的接口的異同

不少面嚮對象語言都有接口這一律念,例如 Java 和 C#。Java 的接口不只能夠定義方法簽名,還能夠定義變量,這些定義的變量能夠直接在實現接口的類中使用:緩存

public interface PersonInterface {
    public String name = "defalut";
    public void sayHello();
}
複製代碼

上述代碼定義了一個必須實現的方法 sayHello 和一個會注入到實現類的變量 name。在下面的代碼中,PersonInterfaceImpl 就實現了 PersonInterface 接口:安全

public class PersonInterfaceImpl implements PersonInterface {
    public void sayHello() {
        System.out.println(MyInterface.hello);
    }
}
複製代碼

Java 中的類必須經過上述方式顯式地聲明實現的接口,可是在 Go 語言中實現接口就不須要使用相似的方式。首先,咱們簡單瞭解一下在 Go 語言中如何定義接口。定義接口須要使用 interface 關鍵字,在接口中咱們只能定義方法簽名,不能包含成員變量,一個常見的 Go 語言接口是這樣的:bash

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

複製代碼

若是一個類型須要實現 Handler 接口,那麼它只須要實現 ServeHTTP(ResponseWriter, *Request)方法,下面的 "github.com/julienschmidt/httprouter" 軟件包的Router結構體就是 ServeHTTP 接口的一個實現:數據結構

// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)
複製代碼

細心的讀者可能會發現上述代碼根本就沒有 Handler 接口的影子,這是爲何呢?Go 語言中接口的實現都是隱式的,咱們只須要實現 ServeHTTP(ResponseWriter, *Request)方法實現了 Handler 接口。Go 語言實現接口的方式與 Java 徹底不一樣:閉包

  • 在 Java 中:實現接口須要顯式的聲明接口並實現全部方法;
  • 在 Go 中:實現接口的全部方法就隱式的實現了接口;

咱們使用上述 Router 結構體時並不關心它實現了哪些接口,Go 語言只會在傳遞參數、返回參數以及變量賦值時纔會對某個類型是否實現接口進行檢查.框架

3. 他的底層原理是什麼樣的?

接口也是 Go 語言中的一種類型,它可以出如今變量的定義、函數的入參和返回值中並對它們進行約束。可是空接口類型interface{}是一個特殊的類型,他可以做爲任何一種類型的接受類型。爲了更好的深刻後面的內容,咱們先來了解一下函數和方法調用: Go中有4種不一樣類型的函數:

  • 頂級函數
  • 值接受者函數
  • 指針接受者函數
  • 函數字面量

5種不一樣類型的調用:

  • 直接調用頂級函數
  • 直接調用值接受者函數
  • 直接調用指針接受者函數
  • 接口上方法的間接調用
  • 函數值的間接調用

它們混合在一塊兒,構成了功能和調用類型的10種可能的組合:

  • 直接調用頂級函數/
  • 直接調用帶有值接收器的方法/
  • 直接調用帶有指針接收器的方法/
  • 接口上方法的間接調用/包含值方法的值/
  • 接口上的方法的間接調用/包含帶有值方法的指針
  • 接口上的方法的間接調用/包含帶有指針方法的指針
  • 間接調用func值/設置爲頂級func
  • 間接調用func值/設置爲value方法
  • 間接調用func值/設置爲指針方法
  • 間接調用func值/設置爲字面量func

(斜槓將編譯時已知的內容與僅在運行時發現的內容分隔開。)

咱們將首先花幾分鐘來回顧這三種直接調用,而後在本章的其他部分中,咱們將重點轉移到接口和間接方法調用上。 咱們不會在本章中介紹函數字面量,由於這樣作首先須要咱們熟悉閉包的機制..咱們將不可避免地在適當的時候這樣作。

3.1的直接調用概述

看一下下面的例子:

package main

func Add(a, b int32) int32 {
	return a + b 
}

type Adder struct{
	id int32 
}
//go:noinline
func (adder *Adder) AddPtr(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) AddVal(a, b int32) int32 {
	return a + b
}

func main() {
    Add(10, 32) // direct call of top-level function

    adder := Adder{id: 6754}
    adder.AddPtr(10, 32) // direct call of method with pointer receiver
    adder.AddVal(10, 32) // direct call of method with value receiver

    (&adder).AddVal(10, 32) // implicit dereferencing
}
複製代碼

讓咱們快速查看爲這4個調用中的每一個調用生成的代碼。

  • 直接調用頂級函數
0x0021 00033 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	PCDATA	$0, $0
	0x0021 00033 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	MOVQ	$137438953482, AX
	0x002b 00043 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	MOVQ	AX, (SP)
	0x002f 00047 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	CALL	"".Add(SB)
	0x0034 00052 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:43)	MOVL	$0, "".adder+24(SP)
複製代碼

正如咱們從第一章已經知道的那樣,咱們看到這轉化爲直接跳轉到.text節中的全局函數符號,並將參數和返回值存儲在調用者的堆棧框架中。

直接調用頂級函數:直接調用頂級函數會傳遞堆棧上的全部參數,並指望結果佔據後續的堆棧位置。

  • 直接調用帶有指針接收器的方法

首先,接收器經過adder := Adder{id: 6754}

0x003c 00060 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:43)	MOVL	$6754, "".adder+24(SP)
複製代碼

(咱們的堆棧幀上的多餘空間已做爲幀指針前導碼的一部分進行了預先分配,爲簡潔起見,此處未顯示。) 而後是對adder.AddPtr(10, 32)的實際方法調用:

0x0044 00068 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	PCDATA	$2, $1
	0x0044 00068 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	LEAQ	"".adder+24(SP), AX
	0x0049 00073 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	PCDATA	$2, $0
	0x0049 00073 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	AX, (SP)
	0x004d 00077 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	$137438953482, AX
	0x0057 00087 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	AX, 8(SP)
	0x005c 00092 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	CALL	"".(*Adder).AddPtr(SB)
複製代碼

查看彙編輸出,咱們能夠清楚地看到,對方法的調用(不管它具備值接收器仍是指針接收器)與函數調用幾乎相同,惟一的區別是接收器做爲第一個參數傳遞。 在這種狀況下,咱們經過在幀的頂部加載有效地址(LEAQ)"".adder+28(SP)的來作到這一點,從而使第一個參數成爲·&adder. 請注意,編譯器如何編碼接收器的類型,以及它是直接在符號名稱中的值仍是指針:

"".(*Adder).AddPtr
複製代碼

直接調用方法:爲了對func值的間接調用和直接調用使用相同的生成代碼,選擇爲方法(值和指針接收器)生成的代碼,使其具備與頂層函數相同的調用約定。 以接收者爲主導。

  • 使用值接收器直接調用方法

如咱們所料,使用值接收器會產生與上面很是類似的代碼。 看一下adder.AddVal(10, 32):

0x0061 00097 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVL	"".adder+24(SP), AX
	0x0065 00101 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVL	AX, (SP)
	0x0068 00104 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVQ	$137438953482, AX
	0x0072 00114 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVQ	AX, 4(SP)
	0x0077 00119 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	CALL	"".Adder.AddVal(SB)
複製代碼

不過,看起來彷佛有些棘手:生成的程序集甚至在任何地方都沒有引用"".adder + 28(SP),即便咱們的接收器當前位於該位置。 那麼,這裏到底發生了什麼? 好吧,因爲接收者是一個值,而且因爲編譯器可以靜態推斷該值,它不會從當前位置(28(SP))複製現有值,而是直接在堆棧上建立一個新的,相同的Adder值,並將此操做與第二個參數的建立合併以保存 在此過程當中再增長一條指令。再次注意該方法的符號名如何明確表示它指望值接收器。

隱式取消引用

咱們尚未看到最後一個調用:(&adder).AddVal(10,32).在這種狀況下,咱們使用指針變量來調用一個指望值接收器的方法。 Go會以某種方式自動取消引用指針並設法進行調用。 爲什麼如此?

編譯器如何處理這種狀況取決於所指向的接收方是否已轉義到堆中。

狀況1:接收者在堆棧上 若是接收器仍在堆棧上,而且其大小足夠小,能夠按幾條指令進行復制(如此處的狀況),則編譯器只需將其值複製到堆棧的頂部,而後對它進行簡單的方法調用便可。 無聊(儘管有效)。 讓咱們繼續進行案例B。

狀況2:接收者在堆上

若是接受者已經逃逸到了堆上,編譯器須要採用一個巧妙的方法:它將產生一個新的方法(帶有指針接受者),包裹"".Adder.AddVal,而且替換原始被包裹者調用"".Adder.AddVal爲一個包裹者調用"".(*Adder).AddVal

所以,包裝器的惟一任務是確保接收者在傳遞給包裝器以前已被正確解除引用,而且確保所涉及的全部參數和返回值都在調用者和包裝器之間正確地來回複製。

注意:在彙編輸出中,這些包裝器方法被標記爲<autogenerated>

下面是生成的包裝器的帶註釋的清單,但願能幫助您理清頭緒

"".(*Adder).AddVal STEXT dupok size=147 args=0x18 locals=0x28
	0x0000 00000 (<autogenerated>:1)	TEXT	"".(*Adder).AddVal(SB), DUPOK|WRAPPER|ABIInternal, $40-24
	... // 省略其餘部分
	0x0026 00038 (<autogenerated>:1)	MOVL	$0, "".~r2+64(SP)
	0x002e 00046 (<autogenerated>:1)	CMPQ	""..this+48(SP), $0 // 檢測接受者是否爲空
	0x0034 00052 (<autogenerated>:1)	JNE	56
	0x0036 00054 (<autogenerated>:1)	JMP	115      // 若是爲nil,跳到115 panic
	0x0038 00056 (<autogenerated>:1)	PCDATA	$2, $1
	0x0038 00056 (<autogenerated>:1)	PCDATA	$0, $1
	0x0038 00056 (<autogenerated>:1)	MOVQ	""..this+48(SP), AX
	0x003d 00061 (<autogenerated>:1)	TESTB	AL, (AX)
	0x003f 00063 (<autogenerated>:1)	PCDATA	$2, $0
	0x003f 00063 (<autogenerated>:1)	MOVL	(AX), AX  // 解引用指針接收器
	0x0041 00065 (<autogenerated>:1)	MOVL	AX, ""..autotmp_5+24(SP)
	0x0045 00069 (<autogenerated>:1)	MOVL	AX, (SP)  // 並將參數值移到參數1
	0x0048 00072 (<autogenerated>:1)	MOVL	"".a+56(SP), AX
	0x004c 00076 (<autogenerated>:1)	MOVL	AX, 4(SP)
	0x0050 00080 (<autogenerated>:1)	MOVL	"".b+60(SP), AX
	0x0054 00084 (<autogenerated>:1)	MOVL	AX, 8(SP)
	0x0058 00088 (<autogenerated>:1)	CALL	"".Adder.AddVal(SB)  // 調用被包裝者方法
	0x005d 00093 (<autogenerated>:1)	MOVL	16(SP), AX  // copy被包裝這返回值
	0x0061 00097 (<autogenerated>:1)	MOVL	AX, ""..autotmp_4+28(SP)
	0x0065 00101 (<autogenerated>:1)	MOVL	AX, "".~r2+64(SP)
	0x0069 00105 (<autogenerated>:1)	MOVQ	32(SP), BP
	0x006e 00110 (<autogenerated>:1)	ADDQ	$40, SP
	0x0072 00114 (<autogenerated>:1)	RET
	0x0073 00115 (<autogenerated>:1)	CALL	runtime.panicwrap(SB)
	0x0078 00120 (<autogenerated>:1)	UNDEF
複製代碼

顯然,考慮到爲了往返傳遞參數而須要進行的全部複製,這種包裝器可能會致使至關多的開銷。 特別是在被包裝的只是一些指令的狀況下。 幸運的是,實際上,編譯器會直接將包裝內聯到包裝器中以分攤這些成本(至少在可行時)。

請注意符號定義中的WRAPPER指令,該指令指示該方法不該出如今回溯中(以避免使最終用戶感到困惑),也不能從被包裝者引起的恐慌中恢復 。

WRAPPER:這是一個包裝函數,不該視爲禁用恢復。

若是包裝的接收者爲nil,則runtime.panicwrap函數會引起恐慌,這很容易解釋。 這是其完整列表,以供參考

// 若是經過一個nil指針接受者調用被包裝的值方法panicwrap將產生恐慌
// 從生成的包裝器代碼中調用它。
func panicwrap() {
	pc := getcallerpc()
	name := funcname(findfunc(pc))
	// name is something like "main.(*T).F".
	// We want to extract pkg ("main"), typ ("T"), and meth ("F").
	// Do it by finding the parens.
	i := bytealg.IndexByteString(name, '(')
	if i < 0 {
		throw("panicwrap: no ( in " + name)
	}
	pkg := name[:i-1]
	if i+2 >= len(name) || name[i-1:i+2] != ".(*" {
		throw("panicwrap: unexpected string after package name: " + name)
	}
	name = name[i+2:]
	i = bytealg.IndexByteString(name, ')')
	if i < 0 {
		throw("panicwrap: no ) in " + name)
	}
	if i+2 >= len(name) || name[i:i+2] != ")." {
		throw("panicwrap: unexpected string after type name: " + name)
	}
	typ := name[:i]
	meth := name[i+2:]
	panic(plainError("value method " + pkg + "." + typ + "." + meth + " called using nil *" + typ + " pointer"))
}
複製代碼

這就是函數和方法調用的所有內容,咱們如今將重點介紹主要內容:接口。

3.2 接口的解析

  • 數據結構的概況 在理解它們如何工做以前,咱們首先須要構建組成接口的數據結構的心智模型,以及它們在內存中是如何佈局的。 爲此,咱們將快速瀏覽一下runtime包,以瞭解從Go實現的角度來看,接口其實是什麼樣子的。

iface結構體

type iface struct {
	tab  *itab
	data unsafe.Pointer
}
複製代碼

所以,接口是一個很是簡單的結構,它維護2個指針:

  • tab保存了一個itab對象的地址,它嵌入了描述接口類型和它所指向的數據類型的數據結構。
  • data是指向該接口保存的值的原始(例如:unsafe)指針。

雖然這個定義很是簡單,但它已經爲咱們提供了一些有價值的信息:由於接口只能保存指針,因此咱們封裝到接口中的任何具體值都必須有它的地址。

一般,這會致使堆分配,由於編譯器採用保守的路由並迫使接收器轉義。

即便標量類型也是如此!

package main


type Addifier interface{ 
	Add(a, b int32) int32 
}

type Adder struct{ 
	name string 
}


//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b 
}

func main() {
    adder := Adder{name: "myAdder"}
    adder.Add(10, 32)	      // doesn't escape Addifier(adder).Add(10, 32) // escapes } 複製代碼
➜  simpletest go tool compile -m demo2.go
demo2.go:14:7: Adder.Add adder does not escape
demo2.go:21:13: Addifier(adder) escapes to heap
<autogenerated>:1: (*Adder).Add .this does not escape
<autogenerated>:1: leaking param: .this
➜  simpletest
複製代碼

咱們能夠清楚地看到,每次建立新的Addifier接口並使用咱們的adder變量對其進行初始化時,實際上都會發生sizeof(Adder)的堆分配。

在本章的後面,咱們將看到與接口一塊兒使用時,即便簡單的標量類型也能夠致使堆分配。

讓咱們將注意力轉向下一個數據結構:itab

// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
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.
}
複製代碼

itab是接口類型的核心。

首先,它嵌入_type,它是運行時內任何Go類型的內部表示。

_type描述類型的每一個方面:其名稱,其特徵(例如大小,對齊方式...),以及某種程度上的行爲方式(例如比較,哈希...)!

在本示例下,_type字段描述了接口保存的值的類型,即data指針指向的值。

其次,咱們找到一個指向interfacetype的指針,它只是_type的包裝,其中包含一些特定於接口的額外信息。

如您所料,inter字段描述了接口自己的類型。

最後,fun數組包含構成接口的虛擬/調度表的函數指針。

請注意,// variable sized的註釋,這意味着聲明此數組的大小可有可無。咱們將在本章後面看到,編譯器負責分配支持該數組的內存,而且獨立於此處指示的大小進行分配。 一樣,運行時始終使用原始指針訪問此數組,所以邊界檢查不適用於此處。

_type數據結構

// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
// ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and
// ../reflect/type.go:/^type.rtype.
type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldalign uint8
	kind       uint8
	alg        *typeAlg
	// gcdata stores the GC type data for the garbage collector.
	// If the KindGCProg bit is set in kind, gcdata is a GC program.
	// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}
複製代碼

如上所述,_type結構給出了Go類型的完整描述。值得慶幸的是,這些字段大多數都是不言而喻的。

nameOfftypeOff類型是連接器嵌入到最終可執行文件中的元數據的int32偏移量。該元數據在運行時加載到runtime.moduledata結構中,若是您曾經查看過ELF文件的內容,那麼它應該看起來很是類似。

運行時提供了一些幫助程序,這些幫助程序實現了必要的邏輯,以便經過moduledata結構跟蹤這些偏移量,例如resolveNameOffresolveTypeOff.

func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}
複製代碼

即,假設t_type,則調用resolveTypeOff(t,t.ptrToThis)返回t的副本。

interfacetype結構體:

type interfacetype struct {
	typ     _type
	pkgpath name
	mhdr    []imethod
}

type imethod struct {
	name nameOff
	ityp typeOff
}
複製代碼

如前所述,interfacetype只是一個_type的包裝器,在其上添加了一些額外的特定於接口的元數據。

在當前的實現中,此元數據主要由偏移量列表組成,這些偏移量指向接口([]imethod)公開的方法的相應名稱和類型。

這是iface內聯全部子類型表示時的外觀的概述。 但願這將有助於鏈接全部的點:

type iface struct { // `iface`
    tab *struct { // `itab`
        inter *struct { // `interfacetype`
            typ struct { // `_type`
                size       uintptr
                ptrdata    uintptr
                hash       uint32
                tflag      tflag
                align      uint8
                fieldalign uint8
                kind       uint8
                alg        *typeAlg
                gcdata     *byte
                str        nameOff
                ptrToThis  typeOff
            }
            pkgpath name
            mhdr    []struct { // `imethod`
                name nameOff
                ityp typeOff
            }
        }
        _type *struct { // `_type`
            size       uintptr
            ptrdata    uintptr
            hash       uint32
            tflag      tflag
            align      uint8
            fieldalign uint8
            kind       uint8
            alg        *typeAlg
            gcdata     *byte
            str        nameOff
            ptrToThis  typeOff
        }
        hash uint32
        _    [4]byte
        fun  [1]uintptr
    }
    data unsafe.Pointer
}
複製代碼

本節介紹構成接口的不一樣數據類型,以幫助咱們開始構建涉及整個機械的各類齒輪的思惟模型,以及它們如何相互配合。

在下一節中,咱們將學習如何實際計算這些數據結構。

3.3 建立一個接口

如今,咱們已經快速瀏覽了全部涉及的數據結構,咱們將集中討論如何實際分配和初始化它們。

package main


type Mather interface {
    Add(a, b int32) int32
    Sub(a, b int64) int64
}

type Adder struct{
	id int32
}
//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) Sub(a, b int64) int64 {
	return a - b
}

func main() {
    m := Mather(Adder{id: 6754})

    // 這個調用僅僅肯定接口是使用的,
    // 沒有這個調用,鏈接器將看到接口是定義了的,可是事實上並無被使用。
    // 並所以會被優化掉
    m.Add(10, 32)
}
複製代碼

注意:接下來,咱們將使用<I,T>標識一個持有T類型的接口I,例如Mather(Adder{id:6754})實例一個iface爲<Mather,Adder>

讓咱們放大一下的實例化iface<Mather, Adder>:

m := Mather(Adder{id: 6754})
複製代碼

這行Go代碼實際上引發了至關多的麻煩,由於編譯器生成的彙編清單能夠證實:

0x001d 00029 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$0, ""..autotmp_1+28(SP)
	0x0025 00037 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$6754, ""..autotmp_1+28(SP)
	0x002d 00045 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$6754, (SP)
	0x0034 00052 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	CALL	runtime.convT32(SB)
	0x0039 00057 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	PCDATA	$2, $1
	0x0039 00057 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	8(SP), AX
	0x003e 00062 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	AX, ""..autotmp_2+32(SP)
	0x0043 00067 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	PCDATA	$2, $2
	0x0043 00067 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	CX, "".m+40(SP)
	0x004f 00079 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	AX, "".m+48(SP)
複製代碼

咱們分爲三部分來講明

  • 1.分配接受者
0x0025 00037 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$6754, ""..autotmp_1+28(SP)
複製代碼

常數十進制值6754(與咱們的AdderID對應)存儲在當前堆棧幀的開頭。它存儲在此處,以便編譯器之後能夠經過其地址引用它。 咱們將在第3部分中瞭解緣由。

  • 2.設置itab
0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $1
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	CX, "".m+40(SP)

複製代碼

看起來編譯器已經建立必要的itab表明iface<Mater,Adder>接口,並使它經過一個全局的代碼go.itab."".Adder,"".Mather,讓咱們可使用。

咱們正在構建iface <Mather,Adder>接口,爲此,咱們正在加載此全局go.itab."".Adder,"".Mather符號的有效地址在當前堆棧幀的頂部。再一次,咱們將在第3部分中看到緣由。

從語義上講,這爲咱們提供瞭如下僞代碼的含義:

tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab)
複製代碼

那就是咱們接口的一半!

如今,在咱們探討它的同時,讓咱們更深刻地瞭解一下go.itab."".Adder,"".Mather 像往常同樣,編譯器的-S標誌能夠告訴咱們不少信息:

go.itab."".Adder,"".Mather SRODATA dupok size=40
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
	rel 0+8 t=1 type."".Mather+0
	rel 8+8 t=1 type."".Adder+0
	rel 24+8 t=1 "".(*Adder).Add+0
	rel 32+8 t=1 "".(*Adder).Sub+0
複製代碼

整齊。 讓咱們逐一分析。

第一部分聲明該符號及其屬性:

go.itab."".Adder,"".Mather SRODATA dupok size=40
複製代碼

和往常同樣,因爲咱們直接查看由編譯器生成的中間目標文件(即,連接器還沒有運行),所以符號名稱仍缺乏程序包名稱。 在這方面沒有新內容。

除此以外,咱們在這裏獲得的是一個40字節的全局對象符號,該符號將存儲在二進制文件的.rodata節中。

請注意dupok指令,該指令告訴連接器該符號在連接時屢次出現是合法的:連接器將不得不任意選擇其中一個。

第二部分是與符號關聯的40字節數據的十六進制轉儲。 即,它是itab結構的序列化表示形式:

0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
複製代碼

如您所見,此時大多數數據只是一堆零。 稍後咱們會看到,連接器將負責填充它們。

請注意,在全部這些零中,其實是如何設置4個字節的,偏移量爲0x10 + 4。 若是咱們回顧一下itab結構的聲明並註釋其字段的各個偏移量:

type itab struct { // 40 bytes on a 64bit arch
    inter *interfacetype // offset 0x00 ($00)
    _type *_type	 // offset 0x08 ($08)
    hash  uint32	 // offset 0x10 ($16)
    _     [4]byte	 // offset 0x14 ($20)
    fun   [1]uintptr	 // offset 0x18 ($24)
			 // offset 0x20 ($32)
}
複製代碼

咱們看到偏移量0x10 + 4與哈希uint32字段匹配:即對應於咱們main.Adder類型的哈希值已經在目標文件中了。

第三部分也是最後一部分列出了連接器的一堆重定位指令:

rel 0+8 t=1 type."".Mather+0
	rel 8+8 t=1 type."".Adder+0
	rel 24+8 t=1 "".(*Adder).Add+0
	rel 32+8 t=1 "".(*Adder).Sub+0
複製代碼

rel 0+8 t=1 type."".Mather+0告訴連接器使用全局對象符號type."".Mather的地址填充首八個字節的內容。

rel 8+8 t=1 type."".Adder+0 使用type."".Adder的地址填充接下來的8字節。等等等等

連接器完成其工做並遵循全部這些指令後,咱們40字節序列化的itab將完成。整體而言,咱們如今正在研究相似於如下僞代碼的內容:

tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab)

// 注意:在構建可執行程序時,連接器將去除符號的`type.`前綴,
因此在二進制.rodata部分符號名將是`main.Mather`和`main.Adder`
// 而不是`type.main.Mather` 和 `type.main.Adder`.
// 在玩轉objdump時不要被這個絆倒。
tab.inter = getSymAddr(`type.main.Mather`).(*interfacetype)
tab._type = getSymAddr(`type.main.Adder`).(*_type)

tab.fun[0] = getSymAddr(`main.(*Adder).Add`).(uintptr)
tab.fun[1] = getSymAddr(`main.(*Adder).Sub`).(uintptr)
複製代碼

咱們已經準備好了一個易於使用的itab,如今,若是咱們只附帶一些數據,那將是一個不錯的,完整的接口。

  • 3.設置數據
0x001d 00029 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	$0, ""..autotmp_1+28(SP)
	0x0025 00037 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	$6754, ""..autotmp_1+28(SP)
	0x002d 00045 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	$6754, (SP)
	0x0034 00052 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	CALL	runtime.convT32(SB)
	0x0039 00057 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $1
	0x0039 00057 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	8(SP), AX
	0x003e 00062 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	AX, ""..autotmp_2+32(SP)
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $2
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$1, $1
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $1
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	CX, "".m+40(SP)
	0x004f 00079 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $0
	0x004f 00079 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	AX, "".m+48(SP)
	0x0054 00084 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+40(SP), AX
複製代碼

在第二部分中,咱們已經將一個十進制常數$6754存儲到""..autotmp_1+28(SP)。這個值將做爲參數傳遞給runtime.convT32,看一下這個函數

func convT32(val uint32) (x unsafe.Pointer) {
	if val == 0 {
		x = unsafe.Pointer(&zeroVal[0])
	} else {
		x = mallocgc(4, uint32Type, false)
		*(*uint32)(x) = val
	}
	return
}
複製代碼

從可執行文件重建Itab

在上一節中,咱們轉儲了go.itab."".Adder,"".Mather直接從編譯器生成的目標文件中查看最終大部分爲零的blob(散列值除外):

go.itab."".Adder,"".Mather SRODATA dupok size=40
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
複製代碼

爲了更好地瞭解如何將數據佈局到連接器生成的最終可執行文件中,咱們將遍歷生成的ELF文件並手動重建構成iface <Mather,Adder>的itab的字節。 但願這將使咱們可以在連接器完成工做後觀察itab的外觀。

首先,讓咱們構建iface二進制文件:GOOS = linux GOARCH = amd64 go build -o iface.bin iface.go

  • 1.尋找.rodata

讓咱們打印部分標題以搜索.rodatareadelf能夠幫助您:

➜  interfacetest GOOS=linux GOARCH=amd64 go build -o main.bin main.go 
➜  interfacetest readelf -St -W main.bin                             
There are 25 section headers, starting at offset 0x1c8:

節頭:
  [號] 名稱
       Type            Address          Off    Size   ES   Lk Inf Al
       旗標
  [ 0] 
       NULL            0000000000000000 000000 000000 00   0   0  0
       [0000000000000000]: 
  [ 1] .text
       PROGBITS        0000000000401000 001000 0517ae 00   0   0 16
       [0000000000000006]: ALLOC, EXEC
  [ 2] .rodata
       PROGBITS        0000000000453000 053000 030b00 00   0   0 32
       [0000000000000002]: ALLOC
複製代碼

咱們真正須要的是該部分的(十進制)偏移量,所以讓咱們應用一些pipe-foo:

➜  interfacetest readelf -St -W main.bin | \ 
  grep -A 1 .rodata | \
  tail -n +2 | \
  awk '{print "ibase=16;"toupper($3)}' | \
  bc
339968
複製代碼

這意味着將315392字節存儲到二進制文件中應將咱們放在.rodata節的開頭。

如今,咱們要作的就是將此文件位置映射到虛擬內存地址。

  • 2.查找.rodata的虛擬內存地址(VMA)

VMA是虛擬地址,一旦二進制文件已由OS加載到內存中,該節將被映射到該虛擬地址。 也就是說,這是咱們在運行時用來引用符號的地址。

➜  interfacetest readelf -St -W main.bin | \ 
  grep -A 1 .rodata | \
  tail -n +2 | \
  awk '{print "ibase=16;"toupper($2)}' | \
  bc
4534272
複製代碼

在這種狀況下,咱們關心VMA的緣由是咱們沒法直接向readelf或objdump請求特定符號(AFAIK)的偏移量。 另外一方面,咱們所能作的就是索要特定符號的VMA。

結合一些簡單的數學運算,咱們應該可以在VMA和偏移量之間創建映射,並最終找到所需符號的偏移量。

所以,這就是到目前爲止咱們所知道的:.rodata節位於ELF文件中的偏移$ 315392(= 0x04d000)處,它將在運行時映射到虛擬地址$ 4509696(= 0x44d000)

如今,咱們須要VMA以及所需符號的大小:

- 它的VMA將(間接)容許咱們在可執行文件中定位它。
- 一旦找到正確的偏移量,它的大小將告訴咱們要提取多少數據。
複製代碼
  • 3.查找VMA和go.itab."".Adder,"".Mather大小

objdump爲咱們提供了那些。

首先,找到符號:

➜  simpletest objdump -t -j .rodata iface.bin | grep "go.itab.main.Adder,main.Mather"
000000000047dcc0 g     O .rodata	0000000000000028 go.itab.main.Adder,main.Mather
複製代碼

而後,以十進制形式獲取其VMA:

➜  simpletest objdump -t -j .rodata iface.bin | \
  grep "go.itab.main.Adder,main.Mather" | \
  awk '{print "ibase=16;"toupper($1)}' | \
  bc
4709568
複製代碼

最後,以十進制形式獲取其大小:

➜  simpletest objdump -t -j .rodata iface.bin | \
  grep "go.itab.main.Adder,main.Mather" | \
  awk '{print "ibase=16;"toupper($5)}' | \
  bc
40
複製代碼

所以go.itab.main.Adder,main.Mather在運行時將映射到虛擬地址$ 4673856(= 0x475140),大小爲40個字節(咱們已經知道,由於它是itab結構的大小)。

  • 4.查找並提取go.itab.main.Adder,main.Mather

這提醒了咱們到目前爲止所知道的:

.rodata offset: 0x04d000 == $339968
.rodata VMA: 0x44d000 == $4534272

go.itab.main.Adder,main.Mather VMA: 0x475140 == $4709568
go.itab.main.Adder,main.Mather size: 0x24 = $40
複製代碼

如今,咱們有了定位go.itab.main.Adder,main.Mather在二進制文件中所需的全部元素。

If $315392 (.rodata's offset) maps to $4509696 (.rodata's VMA) and go.itab.main.Adder,main.Mather's VMA is $4673856, then go.itab.main.Adder,main.Mather's offset within the executable is: sym.offset = sym.vma - section.vma + section.offset = $4673856 - $4509696 + $315392 = $479552.

若是$339968(.rodata的偏移量)映射到$4534272(.rodata的VMA)和go.itab.main.Adder,main.Mather的VMA是$4709568,以後,go.itab.main.Adder,main.Mather在可執行文件的偏移量爲sym.offset = sym.vma - section.vma + section.offset = $4709568 - $4534272 + $339968 = $515264

既然咱們已經知道了數據的偏移量和大小,咱們就能夠取出dd並直接從可執行文件中提取原始字節:

➜  simpletest dd if=iface.bin of=/dev/stdout bs=1 count=40 skip=515264 2>/dev/null | hexdump
0000000 20 01 46 00 00 00 00 00 60 3b 46 00 00 00 00 00
0000010 8a 3d 5f 61 00 00 00 00 d0 fa 44 00 00 00 00 00
0000020 50 fb 44 00 00 00 00 00
0000028
複製代碼

小結: 咱們已經爲iface <Mather,Adder>接口重構了完整的itab。 它所有存在於可執行文件中,只是在等待使用,而且已經包含了運行時使接口表現出咱們所指望的全部信息。

固然,因爲itab主要由指向其餘數據結構的一堆指針組成,所以咱們必須遵循經過dd提取的內容中存在的虛擬地址,以重建完整圖片。

說到指針,咱們如今能夠清楚地查看iface <Mather,Adder>;的虛擬表。 這是go.itab.main.Adder,main.Mather內容的帶註釋版本:

➜  simpletest dd if=iface.bin of=/dev/stdout bs=1 count=40 skip=515264 2>/dev/null | hexdump
0000000 20 01 46 00 00 00 00 00 60 3b 46 00 00 00 00 00
0000010 8a 3d 5f 61 00 00 00 00 d0 fa 44 00 00 00 00 00
# -----------------------
# offset 0x18+8: itab.fun[0]
0000020 50 fb 44 00 00 00 00 00
# -----------------------
# offset 0x20+8: itab.fun[1]
0000028
複製代碼
➜  simpletest objdump -t -j .text iface.bin | grep 000000000044fad0
000000000044fad0 g     F .text	0000000000000079 main.(*Adder).Add
複製代碼
➜  simpletest objdump -t -j .text iface.bin | grep 000000000044fb50
000000000044fb50 g     F .text	000000000000007f main.(*Adder).Sub
複製代碼

毫無心外,iface<Mather, Adder>的虛表包含兩個方法指針:main.(*Adder).Add和主要。main.(*Adder).Sub

4.動態調度

在本節中,咱們最終將介紹接口的主要功能:動態調度。

具體來講,咱們將研究動態調度是如何在後臺進行的,以及咱們須要爲此付出多少。

  • 接口上的間接方法調用
package main 


type Mather interface {
    Add(a, b int32) int32
    Sub(a, b int64) int64
}

type Adder struct{
	id int32
}
//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) Sub(a, b int64) int64 {
	return a - b
}

func main() {
    m := Mather(Adder{id: 6754})

    // This call just makes sure that the interface is actually used.
    // Without this call, the linker would see that the interface defined above
    // is in fact never used, and thus would optimize it out of the final
    // executable.
    m.Add(10, 32)
}
複製代碼

咱們已經對這段代碼中的大部分操做進行了更深刻的研究:iface <Mather,Adder>接口是如何建立的,如何在最終的exectutable中進行佈局,以及最終如何在運行時加載 。

咱們只剩下一件事要看,那就是隨後的實際間接方法調用:m.Add(10,32)

爲了刷新咱們的記憶,咱們將放大接口的建立以及方法調用自己:

m := Mather(Adder{id: 6754})
m.Add(10, 32)
複製代碼

值得慶幸的是,咱們已經有了由第一行的實例化生成的程序集的完整註釋版本(m:= Mather(Adder {id:6754})):

0x0054 00084 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+40(SP), AX
	0x0059 00089 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	TESTB	AL, (AX)
	0x005b 00091 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	24(AX), AX
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	$0, $3
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	$1, $0
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+48(SP), CX
	0x0064 00100 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	$0, $0
	0x0064 00100 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, (SP)
	0x0068 00104 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	$137438953482, CX
	0x0072 00114 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, 8(SP)
	0x0077 00119 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	CALL	AX
複製代碼

藉助前幾節中積累的知識,這幾條說明應該易於理解。

0x005b 00091 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	24(AX), AX
複製代碼

經過解引用AX並向前偏移24個字節,咱們到達i.tab.fun,它對應於虛擬表的第一個條目。這提醒了itab的偏移量表是什麼樣的:

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.
}
複製代碼

如上一節所述,咱們直接從可執行文件中重建了最終的itabiface.tab.fun [0]是指向main.(*Adder).add的指針,是編譯器生成的包裝器 -包裝咱們原始的值接收器main.Adder.add方法的方法。

0x0068 00104 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	$137438953482, CX
	0x0072 00114 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, 8(SP)
複製代碼

咱們在堆棧的頂部存儲10和32,做爲參數#2和#3。

0x0077 00119 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	CALL	AX
複製代碼

最後,設置好全部堆棧後,咱們即可以進行實際的調用。

如今,咱們對接口和虛擬方法調用正常工做所需的整個機器有了清晰的瞭解。

5.interface有哪些常見的特殊狀況和使用技巧?

本節將回顧咱們在處理接口時天天遇到的一些最多見的特殊狀況。

5.1 空接口

空接口的數據結構是您直覺上會想到的:沒有itab的iface。

有兩個緣由:

  • 因爲空接口沒有方法,所以能夠安全地從數據結構中刪除與動態調度有關的全部內容。
  • 隨着虛擬表的消失,空接口自己的類型(不要與它所保存的數據的類型混淆)始終是相同的

注意:相似於用於iface的表示法,咱們將表示類型T的空接口表示爲eface <T>

eface長這樣

type eface struct {
	_type *_type
	data  unsafe.Pointer
}
複製代碼

其中_type保存數據指向的值的類型信息。與預期的同樣,itab已徹底刪除。

雖然空接口只能重用iface數據結構(畢竟它是eface的超集),可是運行時選擇區分這兩個緣由是出於兩個主要緣由:空間效率和代碼清晰度。

在本章的前面(接口的解剖),咱們已經提到,即便將簡單的標量類型(例如整數)存儲到接口中,也會致使堆分配。

如今該是咱們瞭解緣由以及方式的時候了。

package main_test

import (
    "testing"
    "fmt"
)

func BenchmarkEfaceScalar(b *testing.B) {
    var Uint uint32
    b.Run("uint32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Uint = uint32(i)
        }
    })
    fmt.Println(Uint)
    var Eface interface{}
    b.Run("eface32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Eface = uint32(i)
        }
    })
    fmt.Println(Eface)
}
複製代碼
➜  simpletest go test -benchmem -bench=. ./demo3_test.go
goos: darwin
goarch: amd64
BenchmarkEfaceScalar/uint32-4         	2000000000	         0.34 ns/op	       0 B/op	       0 allocs/op
1999999999
BenchmarkEfaceScalar/eface32-4        	100000000	        15.9 ns/op	       4 B/op	       1 allocs/op
99999999
PASS
ok  	command-line-arguments	2.335s
複製代碼
  • 對於簡單的分配操做,這是性能的2個數量級差別,而且
  • 咱們能夠看到第二個基準測試必須在每次迭代中分配4個額外的字節。

顯然,在第二種狀況下,一些隱藏的重操做正在被啓動:咱們須要看一下生成的彙編。

對於第一個基準測試,編譯器將生成與賦值操做徹底同樣的預期結果:

0x000d 00013 (demo3_test.go:12)	MOVL	DX, (AX)
複製代碼

可是,在第二個基準測試中,事情變得更加複雜:

0x003d 00061 (demo3_test.go:18)	CMPQ	264(DX), CX
	0x0044 00068 (demo3_test.go:18)	JLE	129
	0x0046 00070 (demo3_test.go:18)	MOVQ	CX, "".i+16(SP)
	0x004b 00075 (demo3_test.go:19)	MOVL	CX, (SP)
	0x004e 00078 (demo3_test.go:19)	CALL	runtime.convT32(SB)
	0x0053 00083 (demo3_test.go:19)	PCDATA	$2, $2
	0x0053 00083 (demo3_test.go:19)	MOVQ	8(SP), AX
	0x0058 00088 (demo3_test.go:19)	PCDATA	$2, $3
	0x0058 00088 (demo3_test.go:19)	LEAQ	type.uint32(SB), CX
	0x005f 00095 (demo3_test.go:19)	PCDATA	$2, $4
	0x005f 00095 (demo3_test.go:19)	MOVQ	"".&Eface+24(SP), DX
複製代碼

雖然在實踐中一般不會發生將標量值固定在接口中的狀況,可是因爲各類緣由,它多是一項昂貴的操做,所以,瞭解其背後的機制很是重要。

說到成本,咱們已經提到編譯器實現了各類技巧,以免在某些特定狀況下進行分配。 咱們將在本節中快速介紹其中3種技巧。

  • 接口技巧1:字節大小的值
var Eface interface{}
    b.Run("eface32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Eface = uint8(i)
        }
    })
複製代碼
➜  simpletest go test -benchmem -bench=. ./demo3_test.go
goos: darwin
goarch: amd64
BenchmarkEfaceScalar/uint32-4         	2000000000	         0.34 ns/op	       0 B/op	       0 allocs/op
1999999999
BenchmarkEfaceScalar/eface32-4        	2000000000	         1.03 ns/op	       0 B/op	       0 allocs/op
255
PASS
ok  	command-line-arguments	2.883s
複製代碼
0x0041 00065 (demo3_test.go:19)	LEAQ	runtime.staticbytes(SB), R8
複製代碼

咱們注意到,在使用字節大小的值的狀況下,編譯器避免了調用runtime.convT32和關聯的堆分配,而是從新使用了已保存的運行時公開的全局變量的地址。 咱們正在尋找的1個字節的值LEAQ runtime.staticbytes(SB), R8.

  • 2.接口技巧2:靜態推理
var Eface interface{}
    b.Run("eface32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Eface = uint64(65)
        }
    })
複製代碼
➜  simpletest go test -benchmem -bench=. ./demo3_test.go
goos: darwin
goarch: amd64
BenchmarkEfaceScalar/uint32-4         	2000000000	         0.34 ns/op	       0 B/op	       0 allocs/op
1999999999
BenchmarkEfaceScalar/eface32-4        	2000000000	         0.90 ns/op	       0 B/op	       0 allocs/op
65
PASS
ok  	command-line-arguments	2.632s
複製代碼
0x0034 00052 (demo3_test.go:19)	LEAQ	type.uint64(SB), BX
	0x003b 00059 (demo3_test.go:19)	PCDATA	$2, $3
	0x003b 00059 (demo3_test.go:19)	MOVQ	BX, (CX)
	0x003e 00062 (demo3_test.go:19)	PCDATA	$2, $-2
	0x003e 00062 (demo3_test.go:19)	PCDATA	$0, $-2
	0x003e 00062 (demo3_test.go:19)	CMPL	runtime.writeBarrier(SB), $0
	0x0045 00069 (demo3_test.go:19)	JNE	84
	0x0047 00071 (demo3_test.go:19)	LEAQ	"".statictmp_0(SB), SI
	0x004e 00078 (demo3_test.go:19)	MOVQ	SI, 8(CX)
	0x0052 00082 (demo3_test.go:19)	JMP	40
	0x0054 00084 (demo3_test.go:19)	LEAQ	8(CX), DI
	0x0058 00088 (demo3_test.go:18)	MOVQ	AX, SI
	0x005b 00091 (demo3_test.go:19)	LEAQ	"".statictmp_0(SB), AX
	0x0062 00098 (demo3_test.go:19)	CALL	runtime.gcWriteBarrier(SB)
複製代碼

從生成的程序集中咱們能夠看到,編譯器徹底優化了對runtime.conv64的調用,相反,它經過加載已經保存了咱們要查找的值的自動生成的全局變量的地址來直接構造空接口:LEAQ "".statictmp_0(SB),SI(請注意(SB)部分,指示全局變量)。

  • 接口技巧3:零值

對於此最後的技巧,請考慮如下基準,該基準從零值實例化eface <uint32>

var Eface interface{}
    b.Run("eface32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Eface = uint64(i-i)
        }
    })
複製代碼
➜  simpletest go test -benchmem -bench=. ./demo3_test.go
goos: darwin
goarch: amd64
BenchmarkEfaceScalar/uint32-4         	2000000000	         0.37 ns/op	       0 B/op	       0 allocs/op
1999999999
BenchmarkEfaceScalar/eface32-4        	500000000	         3.02 ns/op	       0 B/op	       0 allocs/op
0
PASS
ok  	command-line-arguments	2.636s
複製代碼

首先,請注意咱們如何使用uint32(i-i)而不是uint32(0)來防止編譯器退回到優化#2(靜態推斷)。

正如咱們在剖析runtime.convT32時早先提到的那樣,可使用相似於#1(字節大小的值)的技巧來優化此處的分配:當某些代碼須要引用持有零值的變量時 ,編譯器只給它提供運行時公開的全局變量的地址,該變量的值始終爲零。

const maxZero = 1024 // must match value in cmd/compile/internal/gc/walk.go
var zeroVal [maxZero]byte
複製代碼

6.關於零值的一句話

正如咱們已經看到的,當要由結果接口保存的數據剛好引用零值時,runtime.convT2 *系列函數避免了堆分配。

這種優化並不是特定於接口,其實是Go運行時所作的一項普遍工做,以確保在須要指向零值的指針時,經過獲取特殊的,始終爲-的地址來避免沒必要要的分配。 運行時公開的零變量。

package main

import (
	"fmt"
	"unsafe"
)
//go:linkname zeroVal runtime.zeroVal
var zeroVal uintptr

type eface struct{ 
	_type, 
	data unsafe.Pointer 
}

func main() {
    x := 42
    var i interface{} = x - x // outsmart the compiler (avoid static inference)

    fmt.Printf("zeroVal = %p\n", &zeroVal)
    fmt.Printf(" i = %p\n", ((*eface)(unsafe.Pointer(&i))).data)
}
複製代碼
➜  simpletest go run zero_value.go
zeroVal = 0x118e8c0
      i = 0x118e8c0
複製代碼

7.零大小變量的切線

與零值相似,Go程序中一個很是常見的技巧是依賴於如下事實:實例化大小爲0的對象(例如struct {} {})不會致使分配。

官方的Go規範(在本章末尾連接)以說明此內容的註釋結尾:

若是結構或數組類型不包含大小大於零的字段(或元素),則其大小爲零。 兩個不一樣的零大小變量在內存中可能具備相同的地址。

「可能在內存中具備相同的地址」中的「可能」表示編譯器不保證這個事實是正確的,儘管在官方Go編譯器的當前實現中一直如此而且繼續如此(gc )。

func main() {
    var s struct{}
    var a [42]struct{}

    fmt.Printf("s = % p\n", &s)
    fmt.Printf("a = % p\n", &a)
}
複製代碼
➜  simpletest go run zero_value.go
s =  0x118dfd0
a =  0x118dfd0
複製代碼

若是咱們想知道該地址後面隱藏着什麼,咱們能夠簡單地查看一下二進制文件:

➜  simpletest objdump -t zerobase.bin | grep 118dfd0
000000000118dfd0 l       0e SECT   0c 0000 [__DATA.__noptrbss] runtime.zerobase
複製代碼

runtime/malloc.go

// base address for all 0-byte allocations
var zerobase uintptr
複製代碼
package main

import (
	"fmt"
	"unsafe"
)
//go:linkname zerobase runtime.zerobase
var zerobase uintptr

func main() {
    var s struct{}
    var a [42]struct{}

    fmt.Printf("zerobase = %p\n", &zerobase)
    fmt.Printf(" s = %p\n", &s)
    fmt.Printf(" a = %p\n", &a)
    fmt.Println(unsafe.Pointer(&a))
}
複製代碼
➜  simpletest go run zero_value.go
zerobase = 0x118dfd0
       s = 0x118dfd0
       a = 0x118dfd0
0x118dfd0
複製代碼

8.斷言

咱們將從實現和成本的角度來看待類型斷言

8.1.類型斷言

package main

import (
	"fmt"
)

func main() {
	var j uint32
	var Eface interface{} // outsmart compiler (avoid static inference)

    i := uint64(42)
    Eface = i
    j = Eface.(uint32)
    fmt.Println(j)
}

複製代碼
0x001d 00029 (zero_value.go:13)	LEAQ	type.uint64(SB), AX
	0x0024 00036 (zero_value.go:13)	PCDATA	$2, $0
	0x0024 00036 (zero_value.go:13)	MOVQ	AX, (SP)
	0x0028 00040 (zero_value.go:13)	PCDATA	$2, $1
	0x0028 00040 (zero_value.go:13)	LEAQ	type.uint32(SB), AX
	0x002f 00047 (zero_value.go:13)	PCDATA	$2, $0
	0x002f 00047 (zero_value.go:13)	MOVQ	AX, 8(SP)
	0x0034 00052 (zero_value.go:13)	PCDATA	$2, $1
	0x0034 00052 (zero_value.go:13)	LEAQ	type.interface {}(SB), AX
	0x003b 00059 (zero_value.go:13)	PCDATA	$2, $0
	0x003b 00059 (zero_value.go:13)	MOVQ	AX, 16(SP)
	0x0040 00064 (zero_value.go:13)	CALL	runtime.panicdottypeE(SB)
	0x0045 00069 (zero_value.go:13)	UNDEF
複製代碼
// panicdottypeE is called when doing an e.(T) conversion and the conversion fails.
// have = the dynamic type we have.
// want = the static type we're trying to convert to. // iface = the static type we're converting from.
func panicdottypeE(have, want, iface *_type) {
	panic(&TypeAssertionError{iface, have, want, ""})
}
複製代碼

8.2 類型switch

package main

import (
	"fmt"
)

func main() {
	var j uint32
	var Eface interface{} // outsmart compiler (avoid static inference)

    i := uint32(42)
    Eface = i
    switch v := Eface.(type) {
    case uint16:
        j = uint32(v)
    case uint32:
        j = v
    }
    fmt.Println(j)
}

複製代碼
0x002f 00047 (zero_value.go:8)	MOVL	$0, "".j+56(SP)
	0x0037 00055 (zero_value.go:9)	XORPS	X0, X0
	0x003a 00058 (zero_value.go:9)	MOVUPS	X0, "".Eface+88(SP)
	0x003f 00063 (zero_value.go:11)	MOVL	$42, "".i+60(SP)
	0x0047 00071 (zero_value.go:12)	MOVL	$42, ""..autotmp_6+68(SP)
	0x004f 00079 (zero_value.go:12)	PCDATA	$2, $1
	0x004f 00079 (zero_value.go:12)	LEAQ	type.uint32(SB), AX
	0x0056 00086 (zero_value.go:12)	MOVQ	AX, "".Eface+88(SP)
	0x005b 00091 (zero_value.go:12)	PCDATA	$2, $2
	0x005b 00091 (zero_value.go:12)	LEAQ	""..autotmp_6+68(SP), CX
	0x0060 00096 (zero_value.go:12)	MOVQ	CX, "".Eface+96(SP)
	0x0065 00101 (zero_value.go:13)	PCDATA	$0, $1
	0x0065 00101 (zero_value.go:13)	MOVQ	AX, ""..autotmp_7+104(SP)
	0x006a 00106 (zero_value.go:13)	PCDATA	$2, $1
	0x006a 00106 (zero_value.go:13)	MOVQ	CX, ""..autotmp_7+112(SP)
	0x006f 00111 (zero_value.go:13)	JMP	113
	0x0071 00113 (zero_value.go:13)	PCDATA	$2, $0
	0x0071 00113 (zero_value.go:13)	TESTB	AL, (AX)
	0x0073 00115 (zero_value.go:13)	MOVL	type.uint32+16(SB), AX
	0x0079 00121 (zero_value.go:13)	MOVL	AX, ""..autotmp_9+64(SP)
	0x007d 00125 (zero_value.go:13)	CMPL	AX, $-800397251
	0x0082 00130 (zero_value.go:13)	JEQ	137
	0x0084 00132 (zero_value.go:13)	JMP	462
	0x0089 00137 (zero_value.go:13)	MOVL	$0, "".v+52(SP)
	0x0091 00145 (zero_value.go:13)	PCDATA	$2, $1
	0x0091 00145 (zero_value.go:13)	MOVQ	""..autotmp_7+112(SP), AX
	0x0096 00150 (zero_value.go:13)	PCDATA	$2, $2
	0x0096 00150 (zero_value.go:13)	LEAQ	type.uint32(SB), CX
	0x009d 00157 (zero_value.go:13)	PCDATA	$2, $1
	0x009d 00157 (zero_value.go:13)	CMPQ	""..autotmp_7+104(SP), CX
	0x00a2 00162 (zero_value.go:13)	JEQ	169
	0x00a4 00164 (zero_value.go:13)	JMP	453
	0x00a9 00169 (zero_value.go:13)	PCDATA	$2, $0
	0x00a9 00169 (zero_value.go:13)	MOVL	(AX), AX
	0x00ab 00171 (zero_value.go:13)	MOVL	$1, CX
	0x00b0 00176 (zero_value.go:13)	JMP	178
	0x00b2 00178 (zero_value.go:13)	MOVL	AX, "".v+52(SP)
	0x00b6 00182 (zero_value.go:13)	MOVB	CL, ""..autotmp_8+49(SP)
	0x00ba 00186 (zero_value.go:13)	TESTB	CL, CL
	0x00bc 00188 (zero_value.go:13)	JNE	195
	0x00be 00190 (zero_value.go:13)	JMP	353
	0x00c3 00195 (zero_value.go:16)	PCDATA	$2, $-2
	0x00c3 00195 (zero_value.go:16)	PCDATA	$0, $-2
	0x00c3 00195 (zero_value.go:16)	JMP	197
	0x00c5 00197 (zero_value.go:17)	PCDATA	$2, $0
	0x00c5 00197 (zero_value.go:17)	PCDATA	$0, $0
	0x00c5 00197 (zero_value.go:17)	MOVL	"".v+52(SP), AX
	0x00c9 00201 (zero_value.go:17)	MOVL	AX, "".j+56(SP)
	0x00cd 00205 (zero_value.go:13)	JMP	207
	0x00cf 00207 (zero_value.go:19)	MOVL	"".j+56(SP), AX
	0x00d3 00211 (zero_value.go:19)	MOVL	AX, (SP)
	0x00d6 00214 (zero_value.go:19)	CALL	runtime.convT32(SB)
	0x00db 00219 (zero_value.go:19)	PCDATA	$2, $1
	0x00db 00219 (zero_value.go:19)	MOVQ	8(SP), AX
	0x00e0 00224 (zero_value.go:19)	PCDATA	$2, $0
	0x00e0 00224 (zero_value.go:19)	PCDATA	$0, $2
	0x00e0 00224 (zero_value.go:19)	MOVQ	AX, ""..autotmp_10+80(SP)
	0x00e5 00229 (zero_value.go:19)	PCDATA	$0, $3
	0x00e5 00229 (zero_value.go:19)	XORPS	X0, X0
	0x00e8 00232 (zero_value.go:19)	MOVUPS	X0, ""..autotmp_5+120(SP)
	0x00ed 00237 (zero_value.go:19)	PCDATA	$2, $1
	0x00ed 00237 (zero_value.go:19)	PCDATA	$0, $2
	0x00ed 00237 (zero_value.go:19)	LEAQ	""..autotmp_5+120(SP), AX
	0x00f2 00242 (zero_value.go:19)	MOVQ	AX, ""..autotmp_12+72(SP)
	0x00f7 00247 (zero_value.go:19)	TESTB	AL, (AX)
	0x00f9 00249 (zero_value.go:19)	PCDATA	$2, $2
	0x00f9 00249 (zero_value.go:19)	PCDATA	$0, $0
	0x00f9 00249 (zero_value.go:19)	MOVQ	""..autotmp_10+80(SP), CX
	0x00fe 00254 (zero_value.go:19)	PCDATA	$2, $3
	0x00fe 00254 (zero_value.go:19)	LEAQ	type.uint32(SB), DX
	0x0105 00261 (zero_value.go:19)	PCDATA	$2, $2
	0x0105 00261 (zero_value.go:19)	MOVQ	DX, ""..autotmp_5+120(SP)
	0x010a 00266 (zero_value.go:19)	PCDATA	$2, $1
	0x010a 00266 (zero_value.go:19)	MOVQ	CX, ""..autotmp_5+128(SP)
	0x0112 00274 (zero_value.go:19)	TESTB	AL, (AX)
	0x0114 00276 (zero_value.go:19)	JMP	278
複製代碼

注意1:佈局

  • 咱們找到了一個初始指令塊,該指令塊加載了咱們感興趣的變量的_type,並檢查了nil指針,以防萬一。
  • 而後,咱們獲得N個邏輯塊,每一個邏輯塊對應於原始switch語句中描述的狀況之一。
  • 最後,最後一個塊定義了一種間接跳轉表,該表容許控制流從一種狀況跳轉到另外一種狀況,同時確保在途中正確重置髒寄存器。

儘管過後看來很明顯,但第二點很是重要,由於這意味着類型切換語句生成的指令數量純粹是它描述的案例數量的一個因素。

在實踐中,這可能會致使使人驚訝的性能問題,例如,帶有大量狀況的大規模類型轉換語句可能生成大量指令,而且若是在錯誤的路徑上使用L1i緩存,最終會破壞它們。

關於上面的簡單切換語句的佈局,另外一個有趣的事實是在生成的代碼中設置案例的順序。 在咱們原始的Go代碼中,案例uint16首先出現,而後是案例uint32。 可是,在由編譯器生成的程序集中,它們的順序已顛倒了,如今的狀況是uint32,而第二個是uint16。

在這種特殊狀況下,這種從新排序對咱們是一個淨贏,僅是運氣,AFAICT。 實際上,若是您花時間對類型開關進行一些試驗,尤爲是兩個以上的狀況,您會發現編譯器老是使用某種肯定性啓發式方法來對狀況進行混洗。

注意2:時間複雜度

其次,請注意控制流如何盲目地從一種狀況跳到另外一種狀況,直到它落在評估爲true的狀況下或最終到達switch語句的末尾。

再一次,雖然顯而易見的是,當人們實際上中止考慮它時(「它還能如何工做?」),可是在更高層次的推理中,這很容易被忽略。 在實踐中,這意味着評估類型切換語句的成本隨其案例數線性增長:它是O(n)。

一樣,有效評估具備N個案例的類型轉換語句與評估N個類型聲明具備相同的時間複雜性。 正如咱們所說的,這裏沒有魔術。

注意3:類型hash和指針比較

最後,請注意在每種狀況下如何始終在兩個階段中進行類型比較:

  • 比較類型的哈希值(_type.hash),而後
  • 若是它們匹配,則直接比較每一個_type指針的各自的內存地址。

因爲每一個_type結構都是由編譯器生成的,並存儲在.rodata節的全局變量中,所以咱們能夠確保爲每種類型在程序的生命週期內分配一個惟一的地址。

在這種狀況下,進行額外的指針比較是有意義的,以確保成功的匹配不僅是哈希衝突的結果。但這會引起一個明顯的問題:爲何不直接在指針中比較指針? 首先,徹底放棄類型散列的概念嗎? 尤爲是在咱們前面已經看到的簡單類型斷言中,根本不使用類型哈希。

說到類型哈希,咱們怎麼知道$ -800397251對應於type.uint32.hash,而$ -269349216對應於type.uint16.hash,您可能想知道? 固然很難

package main

import (
	"fmt"
	"unsafe"
)

// simplified definitions of runtime's eface & _type types type eface struct { _type *_type data unsafe.Pointer } type _type struct { size uintptr ptrdata uintptr hash uint32 /* omitted lotta fields */ } var Eface interface{} func main() { Eface = uint32(42) fmt.Printf("eface<uint32>._type.hash = %d\n", int32((*eface)(unsafe.Pointer(&Eface))._type.hash)) Eface = uint16(42) fmt.Printf("eface<uint16>._type.hash = %d\n", int32((*eface)(unsafe.Pointer(&Eface))._type.hash)) } 複製代碼
➜  simpletest go run zero_value.go
eface<uint32>._type.hash = -800397251
eface<uint16>._type.hash = -269349216
複製代碼
相關文章
相關標籤/搜索