Go 不支持 []T转换为[]interface类型详解

目录
  • 正文
  • 官方解释
  • 内存布局
  • 程序运行中的内存布局
  • 通用方法

正文

在 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.

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

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

参考文章:

以上就是Go 不支持 []T 转换为 []interface详解的详细内容,更多关于Go不支持[]T转换[]interface的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go中的关键字any interface是否会成为历史

    目录 引言 示例 增加新关键字后的对比 引言 大家在看 Go1.18 泛型的代码时,不知道是否有留意到一个新的关键字 any. 示例 func Print[T any](s []T) {} 之前没有专门提过,但有没有小伙伴以为这个关键字,是泛型代码专属的? 其实不是...在这次新的 Go1.18 更新中,any 是作为一个新的关键字出现,any 有一个真身,本质上是 interface{} 的别名: type any = interface{} 也就是,在常规代码中,也可以直接使用: func

  • Go interface接口声明实现及作用详解

    目录 什么是接口 接口的定义与作用 接口的声明和实现 接口的声明 接口的实现 接口类型断言 空接口 接口实际用途 通过接口实现面向对象多态特性 通过接口实现一个简单的 IoC (Inversion of Control) 什么是接口 接口是一种定义规范,规定了对象应该具有哪些方法,但并不指定这些方法的具体实现.在 Go 语言中,接口是由一组方法签名(方法名.参数类型.返回值类型)定义的.任何实现了这组方法的类型都可以被认为是实现了这个接口. 这种方式使得接口能够描述任意类型的行为,而不用关心其实

  • go语言interface接口继承多态示例及定义解析

    目录 1.什么是接口 2.接口定义 3.多态 多态加减计算器 4.接口继承与转换 5.空接口 6.接口转换 7.实现map字典接口 8.interface案例 1.什么是接口 接口就是一种规范与标准,在生活中经常见接口,例如:笔记本电脑的USB接口,可以将任何厂商生产的鼠标与键盘,与电脑进行链接.为什么呢?原因就是,USB接口将规范和标准制定好后,各个生产厂商可以按照该标准生产鼠标和键盘就可以了. 在程序开发中,接口只是规定了要做哪些事情,干什么.具体怎么做,接口是不管的.这和生活中接口的案例也

  • GoFrame通用类型变量gvar与interface基本使用对比

    目录 前言摘要 通用变量 gvar 使用场景 看源码学编程 如何设置并发安全开关呢? 基本使用 打印结果 序列化示例 打印结果 总结 前言摘要 这篇文章将介绍 GoFrame 通用类型变量gvar的概念,对比 interface{}的特点:以及如何设置gvar的并发安全开关等基础使用:介绍序列化示例代码. 通用变量 gvar gvar 通用动态变量,支持各种内置的数据类型转换,可以作为interface{}类型的替代数据类型,并且该类型支持并发安全开关. 使用场景 所有需要使用interface

  • Go 不支持 []T转换为[]interface类型详解

    目录 正文 官方解释 内存布局 程序运行中的内存布局 通用方法 正文 在 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

  • Javascript类型系统之String字符串类型详解

    javascript没有表示单个字符的字符型,只有字符串String类型,字符型相当于仅包含一个字符的字符串 字符串String是javascript基本数据类型,同时javascript也支持String对象,它是一个原始值的包装对象.在需要时,javascript会自动在原始形式和对象形式之间转换.本文将介绍字符串String原始类型及String包装对象 定义 字符串String类型是由引号括起来的一组由16位Unicode字符组成的字符序列 字符串类型常被用于表示文本数据,此时字符串中的

  • Java泛型映射不同的值类型详解及实例代码

    Java泛型映射不同的值类型详解 前言: 一般来说,开发人员偶尔会遇到这样的情形: 在一个特定容器中映射任意类型的值.然而Java 集合API只提供了参数化的容器.这限制了类型安全地使用HashMap,如单一的值类型.但如果想混合苹果和梨,该怎样做呢? 幸运的是,有一个简单的设计模式允许使用Java泛型映射不同的值类型,Joshua Bloch在其<Effective Java>(第二版,第29项)中将其描述为类型安全的异构容器(typesafe hetereogeneous Containe

  • javascript类型系统_正则表达式RegExp类型详解

    前面的话 前面已经介绍过javascript中正则表达式的基础语法.javascript的RegExp类表示正则表达式,String和RegExp都定义了方法,使用正则表达式可以进行强大的模式匹配和文本检索与替换.本文将介绍正则表达式的RegExp对象,以及正则表达式涉及 到的属性和方法 对象 javascript中的正则表达式用RegExp对象表示,有两种写法:一种是字面量写法:另一种是构造函数写法 Perl写法 正则表达式字面量写法,又叫Perl写法,因为javascript的正则表达式特性

  • JAVA 中实现整句汉字拆分、转换为ASCII实例详解

    JAVA 中实现整句汉字拆分.转换为ASCII实例详解 大家都知道,一个汉字等于两个byte的大小.二进制数据通过网络传输时,如果两个byte都超过128则会合并成一个Unicode(汉字)字符,本文的代码主要实现的功能是:把这些汉字拆分为byte,然后重新变为ASCII类型的字符串. public static String ChineseToASCII(byte[] rec) { //从字节读取内容 ByteArrayInputStream bais = new ByteArrayInput

  • java @interface 注解详解及实例

    java @interface 注解详解及实例 1 简介 在Java中,定义注解其实和定义接口差多不,只需要在interface前添加一个@符号就可以,即 @interface Zhujie{ },这就表明我们定义了一个名为 @Zhujie 的注解.注解中的每一个方法定义了这个注解类型的一个元素,特别注意:注解中方法的声明中一定不能包含参数,也不能抛出异 常:方法的返回值被限制为简单类型.String.Class.emnus.注释,和这些类型的数组,但方法可以有一个缺省值. 注解相当于一种标记,

  • 易语言的输入字类型详解

    在程序中书写输入字时,可以使用一个半角符号来引导该输入字,以指定其类型.各输入字的类型引导符号为: 首拼及全拼输入字: 分号(";") 如: ;qz(1.23) 或 ;quzheng(1.23) 双拼输入字: 冒号(":") 如: :quvg(1.23) 英文输入字: 单引号(" '") 如: 'int(1.23) 系统具有一个当前默认输入法状态,如果某输入字前没有加上类型引导符号,则默认是属于该输入法的输入字.系统安装完毕后,当前默认输入法为&

  • Golang拾遗之实现一个不可复制类型详解

    目录 如何复制一个对象 为什么要禁止复制 运行时检测实现禁止复制 初步尝试 更好的实现 性能 优点和缺点 静态检测实现禁止复制 利用Locker接口不可复制实现静态检测 优点和缺点 更进一步 利用package和interface进行封装 优点和缺点 总结 如何复制一个对象 不考虑IDE提供的代码分析和go vet之类的静态分析工具,golang里几乎所有的类型都能被复制. // 基本标量类型和指针 var i int = 1 iCopy := i str := "string" st

  • Java基本数据类型与封装类型详解(int和Integer区别)

    int是java提供的8种原始数据类型之一. Java为每个原始类型提供了封装类,Integer是java为int提供的封装类(即Integer是一个java对象,而int只是一个基本数据类型).int的默认值为0,而Integer的默认值为null,即Integer可以区分出未赋值和值为0的区别,int则无法表达出未赋值的情况,例如,要想表达出没有参加考试和考试成绩为0的区别,则只能使用Integer.在JSP开发中,Integer的默认为null,所以用el表达式在文本框中显示时,值为空白字

  • Python的语言类型(详解)

    Python 是强类型的动态脚本语言 . 强类型:不允许不同类型相加 动态:不使用显示数据类型声明,且确定一个变量的类型是在第一次给它赋值的时候 脚本语言:一般也是解释型语言,运行代码只需要一个解释器,不需要编译 强类型语言和弱类型语言 1.强类型语言:使之强制数据类型定义的语言.没有强制类型转化前,不允许两种不同类型的变量相互操作.强类型定义语言是类型安全的语言,如Java.C# 和 python,比如Java中"int i = 0.0;"是无法通过编译的: 2.弱类型语言:数据类型

随机推荐