首页 > 其他分享 >为什么 Go 不支持 []T 转换为 []interface

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

时间:2023-03-28 13:11:10浏览次数:53  
标签:slice 转换 0x00 len interface dlv Go type

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

Go语言中文网 2023-03-27 08:52 发表于北京  

以下文章来源于AlwaysBeta ,作者yongxinz

AlwaysBeta.

大厂程序员,专注分享硬核后端开发技术。每天早上 8 点分享一篇高质量文章,内容包括编程语言、数据库、缓存,架构设计、程序人生等。

在 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.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++ {
    24:   iis[i] = is[i]
    25:  }
    26:
=>  27:  addUpViaInterface(iis)
    28: }

打印 is 的地址:

(dlv) p &is
(*[]int64)(0xc00003a740)

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

(dlv) x -fmt hex -len 32 0xc00003a740
0xc00003a740:   0x10   0xa7   0x03   0x00   0xc0   0x00   0x00   0x00
0xc00003a748:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a750:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a758:   0x00   0x00   0x09   0x00   0xc0   0x00   0x00   0x00

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

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

(dlv) x -fmt hex -len 32 0xc00003a710
0xc00003a710:   0x55   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a718:   0x22   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a720:   0xab   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a728:   0x09   0x00   0x00   0x00   0x00   0x00   0x00   0x00

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

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

(dlv) p &iis
(*[]interface {})(0xc00003a758)
(dlv) x -fmt hex -len 32 0xc00003a758
0xc00003a758:   0x00   0x00   0x09   0x00   0xc0   0x00   0x00   0x00
0xc00003a760:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a768:   0x04   0x00   0x00   0x00   0x00   0x00   0x00   0x00
0xc00003a770:   0xd0   0xa7   0x03   0x00   0xc0   0x00   0x00   0x00

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

(dlv) x -fmt hex -len 64 0xc000090000
0xc000090000:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x00
0xc000090008:   0xa8   0xee   0x0a   0x01   0x00   0x00   0x00   0x00
0xc000090010:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x00
0xc000090018:   0x10   0xed   0x0a   0x01   0x00   0x00   0x00   0x00
0xc000090020:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x00
0xc000090028:   0x58   0xf1   0x0a   0x01   0x00   0x00   0x00   0x00
0xc000090030:   0x00   0xe4   0x05   0x01   0x00   0x00   0x00   0x00
0xc000090038:   0x48   0xec   0x0a   0x01   0x00   0x00   0x00   0x00

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

打印地址内容:

(dlv) x -fmt hex -len 8 0x010aeea8
0x10aeea8:   0x55   0x00   0x00   0x00   0x00   0x00   0x00   0x00
(dlv) x -fmt hex -len 8 0x010aed10
0x10aed10:   0x22   0x00   0x00   0x00   0x00   0x00   0x00   0x00
(dlv) x -fmt hex -len 8 0x010af158
0x10af158:   0xab   0x00   0x00   0x00   0x00   0x00   0x00   0x00
(dlv) x -fmt hex -len 8 0x010aec48
0x10aec48:   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 支持的泛型,这里就不过多介绍了,大家有兴趣的话可以继续研究。

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


参考文章:

  • 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/


推荐阅读

 

福利
我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

图片

阅读 2886     精选留言 写留言
  • 凝雪流冰   来自云南 回复   咋不早点发,昨天才在网上搜的这个问题。[敲打][敲打][敲打]    
  • JOJO   来自广东 回复   这答案确实6    
  • 夜雨街灯   来自上海 回复   golang难用,反射也不好用。不仅仅是效率问题。而是不能用反射达到修改字段值的目的   25条回复  
  • hello哥想变肥仔   来自广东 回复   https://mp.weixin.qq.com/s/vxf-DhjIndrOQjRsGynaCQ 就好像这个特性,应该哪天go团队急眼了,那他们就会随便找个理由来添加上去,比如泛型,什么提案都不如人家刚好需要重要,[捂脸][666][666][旺柴]   2条回复  
  • hello哥想变肥仔   来自广东 回复   感觉你应该先说明一下interface{}.(int)这个过程到底做了什么,不然有点看的蒙。 理论上这个过程仅仅是改变了数据的类型,而不是改变了数据的内存结构,所以官方不想去更改数据的内存结构。 当然也仅此而已,官方只是不想特殊处理这个结构,要是想做,肯定也可以做到的。    
已无更多数据      

标签:slice,转换,0x00,len,interface,dlv,Go,type
From: https://www.cnblogs.com/cheyunhua/p/17264785.html

相关文章

  • Gourmet choice CF1131D
    给你对于任意一个ai,bj的大小关系的判断,让你构造a,b序列满足条件。无解输出No 拓扑排序+并查集 #include<iostream>#include<cstring>#include<queue>usi......
  • MongoDB GridFS最佳应用概述
    《MongoDBGridFS最佳应用概述》作者:chszs,转载需注明。GridFS是MongoDB数据库之上的一个简单文件系统抽象。如果你熟悉AmazonS3的话,那么GridFS与之相似。为什么像MongoDB这......
  • pytest学习和使用22-allure特性 丨总览中的Environment、Categories设置以及Flaky tes
    (22-allure特性丨总览中的Environment和Categories设置)如下图,我们可以看到allure报告的总览,里边的一些特性是可以自定义设置的。1Environment设置Environment可以......
  • 安装 MongoDB
    安装MongoDBhttps://www.mongodb.com/try/download/community如果是Yum安装,可以Package选项选server,然后拷贝链接后使用yum直接安装,如yuminstallhttps://repo.mongo......
  • [重读经典论文]GoogLeNet——Inception模块的诞生
    1.前言GoogLeNet,也被称为InceptionV1网络,由Google公司的研究员在2014年的论文《Goingdeeperwithconvolutions》提出。本论文提出了Inception模块,引入并行结构和不同......
  • go语言学习-并发概念以及goroutine
    进程和线程  进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能......
  • django使用后台admin修改/删除记录的同时更新文件
    问题使用django自带admin后台删除表的时候,因为文件是存在服务器的,所以是只是删除了数据库的数据,而服务器的文件还存在解决models.py#模型类class......
  • window和linux如何将go代码打包成可执行程序
    前言:在window下代码写好了,如何部署到linux呢,或者怎么打包成exe程序,在window下运行呢?查看正文正文:window下如何打包成exe文件,并运行。打开goland,点击Terminal终端输入......
  • go基础语法规则
    前言:go语言基础语法记录正文:1、package package中必须包含一个main的package,并且只能有一个,不然无法编译2、使用import导入包,使用goland会自动导入3、每行的结尾......
  • 一个程序从Google应用市场获取程序信息的Demo
    importjava.io.FileOutputStream;importcom.gc.android.market.api.MarketSession;importcom.gc.android.market.api.MarketSessio......