在 Go 中,如果 interface{}
作为函数参数的话,是可以传任意参数的,然后通过类型断言来转换。
举个例子:
package main
import "fmt"
func foo(v interface{}) {
if v1, ok1 := v.(string); ok1 {
fmt.Println(v1)
} else if v2, ok2 := v.(int); ok2 {
fmt.Println(v2)
}
}
func main() {
foo(233)
foo("666")
}
不管是传 int
还是 string
,最终都能输出正确结果。
那么,既然是这样的话,我就有一个疑问了,拿出我举一反三的能力。是否可以将 []T
转换为 []interface
呢?
比如下面这段代码:
func foo([]interface{}) { /* do something */ }
func main() {
var a []string = []string{"hello", "world"}
foo(a)
}
很遗憾,这段代码是不能编译通过的,如果想直接通过 b := []interface{}(a)
的方式来转换,还是会报错:
cannot use a (type []string) as type []interface {} in function argument
正确的转换方式需要这样写:
b := make([]interface{}, len(a), len(a))
for i := range a {
b[i] = a[i]
}
本来一行代码就能搞定的事情,却非要让人写四行,是不是感觉很麻烦?那为什么 Go 不支持呢?我们接着往下看。
官方解释
这个问题在官方 Wiki 中是有回答的,我复制出来放在下面:
The first is that a variable with type
[]interface{}
is not an interface! It is a slice whose element type happens to be interface{}. But even given this, one might say that the meaning is clear.
Well, is it? A variable with type[]interface{}
has a specific memory layout, known at compile time.
Each interface{} takes up two words (one word for the type of what is contained, the other word for either the contained data or a pointer to it). As a consequence, a slice with length N and with type[]interface{}
is backed by a chunk of data that is N*2 words long.
This is different than the chunk of data backing a slice with type[]MyType
and the same length. Its chunk of data will beN*sizeof(MyType)
words long.
The result is that you cannot quickly assign something of type[]MyType
to something of type[]interface{}
; the data behind them just look different.
大概意思就是说,主要有两方面原因:
[]interface{}
类型并不是interface
,它是一个切片,只不过碰巧它的元素是interface
;[]interface{}
是有特殊内存布局的,跟interface
不一样。
下面就来详细说说,是怎么个不一样。
内存布局
首先来看看 slice 在内存中是如何存储的。在源码中,它是这样定义的:
// src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
array
是指向底层数组的指针;len
是切片的长度;cap
是切片的容量,也就是array
数组的大小。
举个例子,创建如下一个切片:
is := []int64{0x55, 0x22, 0xab, 0x9}
那么它的布局如下图所示:
假设程序运行在 64 位的机器上,那么每个「正方形」所占空间是 8 bytes。上图中的 ptr
所指向的底层数组占用空间就是 4 个「正方形」,也就是 32 bytes。
接下来再看看 []interface{}
在内存中是什么样的。
回答这个问题之前先看一下 interface{}
的结构,Go 中的接口类型分成两类:
iface
表示包含方法的接口;eface
表示不包含方法的空接口。
源码中的定义分别如下:
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
具体细节我们不去深究,但可以明确的是,每个 interface{}
包含两个指针, 会占据两个「正方形」。第一个指针指向 itab
或者 _type
;第二个指针指向实际的数据。
所以它在内存中的布局如下图所示:
因此,不能直接将 []int64
直接传给 []interface{}
。
程序运行中的内存布局
接下来换一个更形象的方式,从程序实际运行过程中,看看内存的分布是怎么样的?
看下面这样一段代码:
package main
var sum int64
func addUpDirect(s []int64) {
for i := 0; i < len(s); i++ {
sum += s[i]
}
}
func addUpViaInterface(s []interface{}) {
for i := 0; i < len(s); i++ {
sum += s[i].(int64)
}
}
func main() {
is := []int64{0x55, 0x22, 0xab, 0x9}
addUpDirect(is)
iis := make([]interface{}, len(is))
for i := 0; i < len(is); i++ {
iis[i] = is[i]
}
addUpViaInterface(iis)
}
我们使用 Delve 来进行调试,可以点击这里进行安装。
dlv debug slice-layout.go
Type 'help' for list of commands.
(dlv) break slice-layout.go:27
Breakpoint 1 set at 0x105a3fe for main.main() ./slice-layout.go:27
(dlv) c
> main.main() ./slice-layout.go:27 (hits goroutine(1):1 total:1) (PC: 0x105a3fe)
22: iis := make([]interface{}, len(is))
23: for i := 0; i < len(is); i