空接口可用于保存任何数据,它可以是一个有用的参数,因为它可以使用任何类型。要理解空接口如何工作以及如何保存任何类型,我们首先应该理解空接口名称背后的概念。

接口(interface{})

对空接口的一个很好的定义:

interface{}implements

因此,空接口作为参数的方法可以接受任何类型。Go 将继续转换为接口类型以满足这个函数。

Russ Cox 撰写了一篇 ,并解释了接口由两个指针组成:

  • 指向类型相关信息的指针
  • 指向数据相关信息的指针

以下是 Russ 在 2009 年画的示意图,:

runtime
func main() {
    var i int8 = 1
    read(i)
}

//go:noinline
func read(i interface{}) {
    println(i)
}
(0x10591e0,0x10be5c6)

两个地址分别代表了类型信息和值的两个指针。

底层结构

reflect/value.go
type emptyInterface struct {
   typ  *rtype            // 类型描述
   word unsafe.Pointer    // 值
}

正如之前解释的那样,我们可以清楚的看到空结构体有一个类型描述字段和一个包含着值的字段。

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
}

在这些字段中,有些非常简单,且广为人知:

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

这个结构还有两个映射,包含字段列表。它清楚地表明,将内建类型转换为空接口将导致扁平转换(译者注:不需要做其他额外的处理),其中字段的描述及值将存储在内存中。

下边是我们看到的空结构体的表示:

结构体由两个指针构成

现在让我们看看空接口实际上可以实现哪种转换。

转换

让我们尝试一个使用空接口的简单程序进行错误转换:

func main() {
    var i int8 = 1
    read(i)
}

//go:noinline
func read(i interface{}) {
    n := i.(int16)
    println(n)
}
int8int16panic
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

让我们生成 代码,以便查看 Go 执行的检查:

有以下几个步骤:

int16空接口CMPQint16LEAQMOVQJNEpanicmain.go:10 +0x7d
interface{}int16int32interface{}-> int32interface{}->int16->int32

性能

下边是两个基准测试。一个使用结构的副本,另一个使用空接口:

package main_test

import (
    "testing"
)

var x MultipleFieldStructure

type MultipleFieldStructure struct {
    a int
    b string
    c float32
    d float64
    e int32
    f bool
    g uint64
    h *string
    i uint16
}

//go:noinline
func emptyInterface(i interface {}) {
    s := i.(MultipleFieldStructure)
    x = s
}

//go:noinline
func 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/op
BenchmarkWithEmptyInterface-8      20000000           60.4 ns/op

与结构副本(typed 函数)相比,使用空接口需要双重转换(原始类型转换为空接口然后再转换回原始类型)多消耗 55 纳秒以上的时间。如果结构中字段的数量增加,时间还会增加:

BenchmarkWithType-8             100000000         17 ns/op
BenchmarkWithEmptyInterface-8    10000000        153 ns/op

但是,有一个好的解决方案是:使用指针并转换回相同的结构指针。转换看起来像下边这样:

func emptyInterface(i interface {}) {
    s := i.(*MultipleFieldStructure)
    y = s
}

和上边相比,结果差异很大:

BenchmarkWithType-8                 2000000000          2.16 ns/op
BenchmarkWithEmptyInterface-8       2000000000          2.02 ns/op
intstring
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

如果使用得当,在大多数情况下,空接口应该会对应用程序的性能产生真正的影响:


本文由 原创编译, 荣誉推出