「Golang系列」 深入理解Golang Empty Interface (空接口)

「Golang系列」 深入理解Golang Empty Interface (空接口)

空接口可用於保存任何數據,它可以是一個有用的參數,因為它可以使用任何類型。 要理解空接口如何工作以及如何保存任何類型,我們首先應該理解名稱背後的概念。

接口

這是Jordan Oreilli對空接口的一個很好的定義:

接口是兩件事:它是一組方法,但它也是一種類型。

interface {}類型是沒有方法的接口。 由於沒有implements關鍵字,所有類型都實現至少零個方法,並且自動滿足接口,所以所有類型都滿足空接口

因此,具有空接口作為參數的方法可以接受任何類型。 Go將轉換為接口類型以提供此功能。

Russ Cox撰寫了一篇關於接口內部表示的精彩文章,並解釋了接口由兩方面組成:

  • 指向存儲類型信息的指針
  • 指向關聯數據的指針

以下是Russ在2009年運行時用C語言編寫的表示:

「Golang系列」 深入理解Golang Empty Interface (空接口)

運行時現在用Go編寫,但表示仍然相同。 我們可以通過打印空接口來驗證:

func main() {
\t\tvar i int8 = 1
\t\tread(i)
\t}
\t
\t//go:noinline
\tfunc read(i interface{}) {
\t\tprintln(i)
\t}
(0x10591e0,0x10be5c6)

兩個地址都代表了類型信息和值的兩個指針。

底層結構

空接口的底層表示形式記錄在反射包中:

type emptyInterface struct {
typ *rtype // word 1 with type description
word unsafe.Pointer // word 2 with the value
}

如前所述,我們清楚地看到空接口有一個類型描述字段,後面跟著包含數據字段。

rtype結構包含類型描述的基礎:

type rtype struct {
size uintptr
ptrdata uintptr
hash uint32

tflag tflag
align uint8
fieldAlign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}

在這些字段中,有些字段非常簡單且眾所周知:

  • size是以字節為單位的大小
  • kind包含類型:int8,int16,bool等。
  • align是有類型變量的對齊方式

根據空接口嵌入的類型,我們可以映射導出的字段或列出方法:

type structType struct {
rtype
pkgPath name
fields []structField
}

該結構體還有兩個映射,包括字段列表。 它清楚地表明,將內置類型轉換為空接口將導致平面轉換,其中字段的描述及其值將存儲在內存中。

這是我們看到的空接口的表示:

「Golang系列」 深入理解Golang Empty Interface (空接口)

現在讓我們看看空接口實際上可以實現哪種轉換。

轉換

讓我們嘗試一個使用空接口錯誤轉換的簡單程序:

func main() {
\t\tvar i int8 = 1
\t\tread(i)
\t}
\t
\t//go:noinline
\tfunc read(i interface{}) {
\t\tn := i.(int16)
\t\tprintln(n)
\t}

雖然從int8到int16的轉換是有效的,但程序會panic:

panic: interface conversion: interface {} is int8, not int16
goroutine 1 [running]:
main.read(0x10592e0, 0x10be5c1)
main.go:10 +0x7d
main.main()
main.go:5 +0x39
exit status 2

讓我們生成asm代碼,以便查看Go執行的檢查:

「Golang系列」 深入理解Golang Empty Interface (空接口)

以下是不同的步驟:

  • 步驟1:比較(指令CMPQ)類型int16(加載指令LEAQ,加載有效地址)到空接口的內部類型(指令MOVQ從空的存儲器段讀取具有48字節偏移量的存儲器) 接口)
  • 步驟2:JNE指令,如果不等於跳轉,將跳轉到將在步驟3中處理錯誤的生成指令
  • 步驟3:代碼將發生panic並生成我們之前看到的錯誤消息
  • 步驟3:這是錯誤指令的結束。 此特定指令由顯示指令的錯誤消息引用:main.go:10 + 0x7d

任何從空接口的內部類型轉換都應該在轉換原始類型之後進行。 這種轉換為空接口然後轉換回原始類型會導致程序成本降低。 讓我們運行一些基準來粗略瞭解它。

性能

這是兩個基準。 一個使用結構的副本,另一個使用空接口:

package main_test
\t
\timport (
\t\t"testing"
\t)
\t
\tvar x MultipleFieldStructure
\t
\ttype MultipleFieldStructure struct {
\t\ta int

\t\tb string
\t\tc float32
\t\td float64
\t\te int32
\t\tf bool
\t\tg uint64
\t\th *string
\t\ti uint16
\t}
\t
\t//go:noinline
\tfunc emptyInterface(i interface {}) {
\t\ts := i.(MultipleFieldStructure)
\t\tx = s
\t}
\t
\t//go:noinline
\tfunc typed(s MultipleFieldStructure) {
\t\tx = s
\t}
\t
\tfunc BenchmarkWithType(b *testing.B) {
\t\ts := MultipleFieldStructure{a: 1, h: new(string)}
\t\tfor i := 0; i < b.N; i++ {
\t\t\ttyped(s)
\t\t}
\t}
\t
\tfunc BenchmarkWithEmptyInterface(b *testing.B) {
\t\ts := MultipleFieldStructure{a: 1, h: new(string)}
\t\tfor i := 0; i < b.N; i++ {
\t\t\temptyInterface(s)
\t\t}
\t}

執行結果:

BenchmarkWithType-8 300000000 4.24 ns/op
BenchmarkWithEmptyInterface-8 20000000 60.4 ns/op

類型轉換到空接口然後在轉換到類型這樣雙轉換比複製結構體要多花費55納秒。時間會隨著結構中的字段數增加而增加:

BenchmarkWithType-8 100000000 17 ns/op 

BenchmarkWithEmptyInterface-8 10000000 153 ns/op

但是,一個好的解決方案是使用指針並轉換回相同的結構體指針。 轉換看起來像這樣:

func emptyInterface(i interface {}) {
\t\ts := i.(*MultipleFieldStructure)
\t\ty = s
\t}

現在結果就完全不同了:

BenchmarkWithType-8 2000000000 2.16 ns/op
BenchmarkWithEmptyInterface-8 2000000000 2.02 ns/op

至於像int或string這樣的基礎類型,性能略有不同:

BenchmarkWithTypeInt-8 2000000000 1.42 ns/op
BenchmarkWithEmptyInterfaceInt-8 1000000000 2.02 ns/op

BenchmarkWithTypeString-8 1000000000 2.19 ns/op
BenchmarkWithEmptyInterfaceString-8 50000000 30.7 ns/op

在大多數情況下,空接口應該對應用程序的性能會產生影響。


分享到:


相關文章: