空接口可用于保存任何数据,它可以是一个有用的参数,因为它可以使用任何类型。 要理解空接口如何工作以及如何保存任何类型,我们首先应该理解名称背后的概念。
接口
这是Jordan Oreilli对空接口的一个很好的定义:
接口是两件事:它是一组方法,但它也是一种类型。
interface {}类型是没有方法的接口。 由于没有implements关键字,所有类型都实现至少零个方法,并且自动满足接口,所以所有类型都满足空接口
因此,具有空接口作为参数的方法可以接受任何类型。 Go将转换为接口类型以提供此功能。
Russ Cox撰写了一篇关于接口内部表示的精彩文章,并解释了接口由两方面组成:
- 指向存储类型信息的指针
- 指向关联数据的指针
以下是Russ在2009年运行时用C语言编写的表示:
运行时现在用Go编写,但表示仍然相同。 我们可以通过打印空接口来验证:
func main() {var i int8 = 1read(i)}//go:noinlinefunc read(i interface{}) {println(i)}
(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}
该结构体还有两个映射,包括字段列表。 它清楚地表明,将内置类型转换为空接口将导致平面转换,其中字段的描述及其值将存储在内存中。
这是我们看到的空接口的表示:
现在让我们看看空接口实际上可以实现哪种转换。
转换
让我们尝试一个使用空接口错误转换的简单程序:
func main() {var i int8 = 1read(i)}//go:noinlinefunc read(i interface{}) {n := i.(int16)println(n)}
虽然从int8到int16的转换是有效的,但程序会panic:
panic: interface conversion: interface {} is int8, not int16goroutine 1 [running]:main.read(0x10592e0, 0x10be5c1)main.go:10 +0x7dmain.main()main.go:5 +0x39exit status 2
让我们生成asm代码,以便查看Go执行的检查:
以下是不同的步骤:
- 步骤1:比较(指令CMPQ)类型int16(加载指令LEAQ,加载有效地址)到空接口的内部类型(指令MOVQ从空的存储器段读取具有48字节偏移量的存储器) 接口)
- 步骤2:JNE指令,如果不等于跳转,将跳转到将在步骤3中处理错误的生成指令
- 步骤3:代码将发生panic并生成我们之前看到的错误消息
- 步骤3:这是错误指令的结束。 此特定指令由显示指令的错误消息引用:main.go:10 + 0x7d
任何从空接口的内部类型转换都应该在转换原始类型之后进行。 这种转换为空接口然后转换回原始类型会导致程序成本降低。 让我们运行一些基准来粗略了解它。
性能
这是两个基准。 一个使用结构的副本,另一个使用空接口:
package main_testimport ("testing")var x MultipleFieldStructuretype MultipleFieldStructure struct {a intb stringc float32d float64e int32f boolg uint64h *stringi uint16}//go:noinlinefunc emptyInterface(i interface {}) {s := i.(MultipleFieldStructure)x = s}//go:noinlinefunc typed(s MultipleFieldStructure) {x = s}func BenchmarkWithType(b *testing.B) {s := MultipleFieldStructure{a: 1, h: new(string)}for i := 0; i < b.N; i++ {typed(s)}}func BenchmarkWithEmptyInterface(b *testing.B) {s := MultipleFieldStructure{a: 1, h: new(string)}for i := 0; i < b.N; i++ {emptyInterface(s)}}
执行结果:
BenchmarkWithType-8 300000000 4.24 ns/opBenchmarkWithEmptyInterface-8 20000000 60.4 ns/op
类型转换到空接口然后在转换到类型这样双转换比复制结构体要多花费55纳秒。时间会随着结构中的字段数增加而增加:
BenchmarkWithType-8 100000000 17 ns/opBenchmarkWithEmptyInterface-8 10000000 153 ns/op
但是,一个好的解决方案是使用指针并转换回相同的结构体指针。 转换看起来像这样:
func emptyInterface(i interface {}) {s := i.(*MultipleFieldStructure)y = s}
现在结果就完全不同了:
BenchmarkWithType-8 2000000000 2.16 ns/opBenchmarkWithEmptyInterface-8 2000000000 2.02 ns/op
至于像int或string这样的基础类型,性能略有不同:
BenchmarkWithTypeInt-8 2000000000 1.42 ns/opBenchmarkWithEmptyInterfaceInt-8 1000000000 2.02 ns/opBenchmarkWithTypeString-8 1000000000 2.19 ns/opBenchmarkWithEmptyInterfaceString-8 50000000 30.7 ns/op
在大多数情况下,空接口应该对应用程序的性能会产生影响。