在 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 支持的泛型,这里就不过多介绍了,大家有兴趣的话可以继续研究。

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

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


参考文章:

推荐阅读:

为什么 Go 不支持 []T 转换为 []interface的更多相关文章

  1. interior转换为interface

    在计算的过程中,我们想要将interior(内部面)转换为interface,操作如下:

  2. Golang高效实践之interface、reflection、json实践

    前言 反射是程序校验自己数据结构和类型的一种机制.文章尝试解释Golang的反射机制工作原理,每种编程语言的反射模型都是不同的,有很多语言甚至都不支持反射. Interface 在将反射之前需要先介绍 ...

  3. 浅析Go语言的Interface机制

    前几日一朋友在学GO,问了我一些interface机制的问题.试着解释发现自己也不是太清楚,所以今天下午特意查了资料和阅读GO的源码(基于go1.4),整理出了此文.如果有错误的地方还望指正. GO语 ...

  4. jupyter notebook 目录配置、导出 tex 和 pdf 及中文支持

    环境:macbook pro, mactex, jupyter notebook, brew 安装pandoc从而支持格式转换为tex: brew install pandoc 修改tex artic ...

  5. Golang 之 interface接口全面理解

    什么是interface 在面向对象编程中,可以这么说:“接口定义了对象的行为”, 那么具体的实现行为就取决于对象了. 在Go中,接口是一组方法签名(声明的是一组方法的集合).当一个类型为接口中的所有 ...

  6. JAVA实现汉字转换为拼音 pinyin4j/JPinyin

    在项目中经常会遇到需求用户输入汉字后转换为拼音的场景,比如说通讯录,就会要求按名字首字符发音排序,如果自己写实现这方面的功能是个很好大的工程,还好网上有公开的第三方jar支持转换,结合网上很多前辈的代 ...

  7. iOS开发零碎知识点

    记录一些常用和不常用的iOS知识点,防止遗忘丢失.(来源为收集自己项目中用到的或者整理看到博客中的知识点),如有错误,欢迎大家批评指正:如有好的知识点,也欢迎大家联系我,添加上去.谢谢! 一.调用代码 ...

  8. C# 泛型的协变和逆变

    1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...

  9. GO语言练习:构建json 和 解析JSON 实例

    本文介绍如何使用Go语言自带的库把对象转换为JSON格式,并在channel中进行传输后,并把JSON格式的信息转换回对象. 1.Go语言的JSON 库 Go语言自带的JSON转换库为 encodin ...

  10. Android中的动态加载机制

    在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...

随机推荐

  1. [转]VB中资源文件.res的使用方法详解

    来源:https://blog.csdn.net/miaozk2006/article/details/82417156 在几乎所有的Windows应用程序中都拥有资源文件,这些文件定义使用应用程序将 ...

  2. Nginx负载均衡策略的介绍与调优

    工作中经常会用到nginx负载均衡这一块,下面对nginx负载均衡策略做个总结.本人在工作中最常用到的负载均衡策略是轮询策略. 在一般情况下,Web中间件最大的作用就是负责对请求进行分发,也就是我们常 ...

  3. 将自己的组件打包发布到npm

    在项目中有些组件在各个项目中都会调用,那么将组件发布到npm ,用到的项目去下载,这样会省去一些不必要的麻烦. 将组件发布到npm 中的步骤 做个记录 1.项目的创建,我这里使用 vue init w ...

  4. jvm之垃圾收集二之常用垃圾收集器

      前面简单介绍了如何确定对象是垃圾.什么时候回收.怎么回收,今天就来聊一聊java中常见的垃圾回收器,从Serial到G1,其中会着重解读CMS和G1的工作原理,包括如何安全的并发回收.cSet.r ...

  5. 强连通分量与tarjan算法初步运用

    模板题:B3609 [图论与代数结构 701] 强连通分量 题目描述 给定一张 n 个点 m 条边的有向图,求出其所有的强连通分量. 注意,本题可能存在重边和自环. 输入格式 第一行两个正整数 n , ...

  6. 嵌入式-Linux基础操作

    Crtl+Alt+T:调出命令窗口 xrandr:列出分辨率列表 设置窗口的分辨率大小为1280x960:xrandr -s 1280x960 通过命令窗口来执行一段C语言程序: VI工具的使用: ( ...

  7. fuzor2020安装教程

    fuzor下载安装包fuzor2020安装教程Fuzor2020 WIN10 64位安装步骤:1.先使用"百度网盘客户端"下载Fur20_CN_x64安装包到电脑磁盘里,并鼠标右击 ...

  8. python编程学习方法及计算机基础理论

    **从零开始学习编程 ** 一.学习前语 在学习python之前首先先说几点学习建议,首先是培养自己能解决问题的能力: 1.遇到问题时给自己设置一个解决该问题的时间限制 0-5min:自己解决问题(百 ...

  9. vim快捷键及命令大全

    定位光标: G 将光标定位到文本末尾行首 gg 将光标定位到文本启始位置 0 (这个是零)定位到光标所在行行首 $ 定位到光标所在行行尾 数字G 跳转到第n行 移动光标: h 向左移动 l 向右移动 ...

  10. Kafka教程(一)基础入门:基本概念、安装部署、运维监控、命令行使用

    Kafka教程(一)基础入门   1.基本概念   背景   领英->Apache   分布式.消息发布订阅系统   角色   存储系统   消息系统   流处理平台-Kafka Streami ...