在 Go 中,如果 interface{}
作为函数参数的话,是能够传任意参数的,而后通过类型断言来转换。
举个例子:
package mainimport "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.gotype 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 mainvar sum int64func 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/que...
- https://github.com/golang/go/...
- https://eli.thegreenplace.net...
举荐浏览:
- 工作流引擎架构设计
- Git 分支管理策略