写点什么

为什么 Go 不支持 []T 转换为 []interface

作者:AlwaysBeta
  • 2023-01-30
    北京
  • 本文字数:3911 字

    阅读完需:约 13 分钟

在 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 be N*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.


大概意思就是说,主要有两方面原因:


  1. []interface{} 类型并不是 interface,它是一个切片,只不过碰巧它的元素是 interface

  2. []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 中的接口类型分成两类:


  1. iface 表示包含方法的接口;

  2. 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.goType 'help' for list of commands.(dlv) break slice-layout.go:27Breakpoint 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++ {    24:      iis[i] = is[i]    25:    }    26:=>  27:    addUpViaInterface(iis)    28:  }
复制代码


打印 is 的地址:


(dlv) p &is(*[]int64)(0xc00003a740)
复制代码


接下来看看 slice 在内存中都包含了哪些内容:


(dlv) x -fmt hex -len 32 0xc00003a7400xc00003a740:   0x10   0xa7   0x03   0x00   0xc0   0x00   0x00   0x000xc00003a748:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x000xc00003a750:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x000xc00003a758:   0x00   0x00   0x09   0x00   0xc0   0x00   0x00   0x00
复制代码


每行有 8 个字节,也就是上文说的一个「正方形」。第一行是指向数据的地址;第二行是 4,表示切片长度;第三行也是 4,表示切片容量。


再来看看指向的数据到底是怎么存的:


(dlv) x -fmt hex -len 32 0xc00003a7100xc00003a710:   0x55   0x00   0x00   0x00   0x00   0x00   0x00   0x000xc00003a718:   0x22   0x00   0x00   0x00   0x00   0x00   0x00   0x000xc00003a720:   0xab   0x00   0x00   0x00   0x00   0x00   0x00   0x000xc00003a728:   0x09   0x00   0x00   0x00   0x00   0x00   0x00   0x00
复制代码


这就是一片连续的存储空间,保存着实际数据。


接下来用同样的方式,再来看看 iis 的内存布局。


(dlv) p &iis(*[]interface {})(0xc00003a758)(dlv) x -fmt hex -len 32 0xc00003a7580xc00003a758:   0x00   0x00   0x09   0x00   0xc0   0x00   0x00   0x000xc00003a760:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x000xc00003a768:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x000xc00003a770:   0xd0   0xa7   0x03   0x00   0xc0   0x00   0x00   0x00
复制代码


切片的布局和 is 是一样的,主要的不同是所指向的数据:


(dlv) x -fmt hex -len 64 0xc0000900000xc000090000:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x000xc000090008:   0xa8   0xee   0x0a   0x01   0x00   0x00   0x00   0x000xc000090010:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x000xc000090018:   0x10   0xed   0x0a   0x01   0x00   0x00   0x00   0x000xc000090020:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x000xc000090028:   0x58   0xf1   0x0a   0x01   0x00   0x00   0x00   0x000xc000090030:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x000xc000090038:   0x48   0xec   0x0a   0x01   0x00   0x00   0x00   0x00
复制代码


仔细观察上面的数据,偶数行内容都是相同的,这个是 interface{}itab 地址。奇数行内容是不同的,指向实际的数据。


打印地址内容:


(dlv) x -fmt hex -len 8 0x010aeea80x10aeea8:   0x55   0x00   0x00   0x00   0x00   0x00   0x00   0x00(dlv) x -fmt hex -len 8 0x010aed100x10aed10:   0x22   0x00   0x00   0x00   0x00   0x00   0x00   0x00(dlv) x -fmt hex -len 8 0x010af1580x10af158:   0xab   0x00   0x00   0x00   0x00   0x00   0x00   0x00(dlv) x -fmt hex -len 8 0x010aec480x10aec48:   0x09   0x00   0x00   0x00   0x00   0x00   0x00   0x00
复制代码


很明显,通过打印程序运行中的状态,和我们的理论分析是一致的。

通用方法

通过以上分析,我们知道了不能转换的原因,那有没有一个通用方法呢?因为我实在是不想每次多写那几行代码。


也是有的,用反射 reflect,但是缺点也很明显,效率会差一些,不建议使用。


func InterfaceSlice(slice interface{}) []interface{} {  s := reflect.ValueOf(slice)  if s.Kind() != reflect.Slice {    panic("InterfaceSlice() given a non-slice type")  }
// Keep the distinction between nil and empty slice input if s.IsNil() { return nil }
ret := make([]interface{}, s.Len())
for i := 0; i < s.Len(); i++ { ret[i] = s.Index(i).Interface() }
return ret}
复制代码


还有其他方式吗?答案就是 Go 1.18 支持的泛型,这里就不过多介绍了,大家有兴趣的话可以继续研究。


以上就是本文的全部内容,如果觉得还不错的话欢迎点赞转发关注,感谢支持。


微信搜索「AlwaysBeta」,第一时间获取文章更新。




参考文章:


  • https://stackoverflow.com/questions/12753805/type-converting-slices-of-interfaces

  • https://github.com/golang/go/wiki/InterfaceSlice

  • https://eli.thegreenplace.net/2021/go-internals-invariance-and-memory-layout-of-slices/


推荐阅读:


发布于: 刚刚阅读数: 6
用户头像

AlwaysBeta

关注

微信公众号:AlwaysBeta 2017-11-30 加入

专注分享后端开发技术干货!

评论

发布
暂无评论
为什么 Go 不支持 []T 转换为 []interface_Go_AlwaysBeta_InfoQ写作社区