深刻理解Go的interface{}內部執行原理

概念補充

Go的interface是由兩種類型來實現的:ifaceeface數組

iface指的是接口中申明有方法(至少1個),eface表示接口中沒有申明方法緩存

後面會講到這兩個究竟是什麼,因此這裏須要先不用關心。函數

深刻理解

下面是一個簡單的Demo,Binary實現了fmt.Stringer接口,咱們調用ToString()方法,會調用接口的String()方法。學習

// 類型
type Binary uint64

// 實現String方法,實現fmt.Stringer接口
func (i Binary) String() string {
	return strconv.FormatUint(uint64(i), 10)
}

func main() {
	b := Binary(200) // 01
  // var b Binary = Binary(200)
	ToString(b)// 02
}
func ToString(value interface{}) string {
  // 斷言,轉化成fmt.Stringer接口類型
	newValue, ok := value.(fmt.Stringer) //3
	if ok {
		return newValue.String()// 4
	}
	panic("The value is not implement fmt.Stringer func")
}
複製代碼

大體的執行流程圖下所示:ui

//01 執行的是,在內存中開闢一塊內存,存放200這個值spa

image-20200619132543003

// 02 調用ToString方法,首先方法傳遞過程當中須要隱式將b轉換成interface{}類型,實際上作的就是如下:3d

首先你們可能會關心,我就沒見過這個結構,你是否是騙人的,其實有這個結構體,是在runtime/runtime2.go指針

type eface struct {
	_type *_type // 類型
	data  unsafe.Pointer //值
}
複製代碼

那麼如何轉換的呢?code

type 指得是 Binary的類型,包含了Binary類型的全部信息(後面會介紹到)orm

data 指向的真實數據,因爲咱們傳遞的不是指針,因此這種狀況下實際上是作了一次內存拷貝(因此也就是儘量的別使用interface{}),data其實存的是拷貝的數據,若是換作是指針,其實也是拷貝了一份指針地址(這也就是reflect.Elem方法的做用)

​ 如下這幾段代碼所有來自於 runtime/iface.go

// 關於 unsafe.Pointer,unsafe包學習的時候介紹過
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
	if raceenabled {
		raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
	}
	if msanenabled {
		msanread(elem, t.size)
	}
  // 首先會分配一塊內存,內存大小爲類型t的大小,下面這段話是mallocgc的介紹
  // Allocate an object of size bytes.
  // Small objects are allocated from the per-P cache's free lists.
  // Large objects (> 32 kB) are allocated straight from the heap.
	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.
  // 將elem拷貝到x 
	typedmemmove(t, x, elem)
  // eface 的類型爲t,值爲x
	e._type = t
	e.data = x
	return
}
複製代碼

//3 其次就是到了斷言的部分,那麼斷言到底執行了什麼呢?

// inter 指的是fmt.Stringer接口類型信息
// e 就是咱們上面的的interface{} 的真實類型eface
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
	t := e._type
	if t == nil {
		return
	}
  // 獲取tab,其實你們有可能不太理解
	tab := getitab(inter, t, true)
	if tab == nil {
		return
	}
	r.tab = tab
	r.data = e.data
	b = true
	return
}
複製代碼

那麼這裏就須要理解什麼是 iface

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

tab 又是什麼?

​ tab的意思是table的意思,關於table的概念,你們能夠去找找資料

​ 具備方法的語言一般屬於如下兩種陣營之一:爲全部方法調用靜態地準備表(如在C ++和Java中),或在每次調用時進行方法查找(如在Smalltalk及其許多模仿程序中,包括JavaScript和Python)以及添加奇特的緩存以提升調用效率。Go位於二者的中間:它具備方法表,但在運行時對其進行計算。

type itab struct {
	inter *interfacetype// 接口類型,這裏就是Stringer
	_type *_type// 值類型, 這裏就是Binary
	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.
}
複製代碼

其實上面這段代碼的流程以下:

data就是 eface.data

tab其實就是 : inter 指的是接口類型(也就是fmt.Stringer接口),type是Binary類型,fun[0]是(Binary)String方法 ,其餘幾個先不用care

image-20200619132703793

//4 newValue.String()到底作了啥,其實根據上面咱們很容易知道,沒法就是newValue.tab.fun[0].(newValue.data) ,因此就是這麼簡單。

總結

一、go的 interface{} 轉換過程當中至少作一次內存拷貝,因此傳遞指針是最好的選擇。

type User struct {
	Name string
	Age  int
}

func main() {
  var empty interface{} = User{} //這裏會拷貝一次,將user轉換成interface{},因此函數傳遞過程當中也別直接使用結構體傳遞
}
複製代碼

正確寫法

func main() {
	var empty interface{} = &User{}
}
複製代碼

二、有人會問到字符串傳遞是否內存拷貝,回答否,由於字符串底層是一個byte[] 數組,他的結構組成是

type StringHeader struct {
	Data uintptr //數據,是一個二進制數組
	Len  int// 長度
}
複製代碼

因此64位計算機,字符串的長度是128位,佔用16個字節

三、減小使用interface{} ,由於會有沒必要要的開銷,其次Golang自己是一個強類型語言,靜態語言,申明式是最好的方式。

四、interface{}是反射的核心,後期我會講解反射

參考

research.swtch.com/interfaces

相關文章
相關標籤/搜索