当前位置: 首页 > news >正文

【Golang】Golang进阶系列教程--为什么 Go 不支持 []T 转换为 []interface

文章目录

  • 前言
  • 官方解释
  • 内存布局
  • 程序运行中的内存布局
  • 通用方法

前言

在 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 N2 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.gotype slice struct {array unsafe.Pointerlen   intcap   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  *itabdata unsafe.Pointer
}
type eface struct {_type *_typedata  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.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 inputif 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 支持的泛型,这里就不过多介绍了,大家有兴趣的话可以继续研究。

相关文章:

【Golang】Golang进阶系列教程--为什么 Go 不支持 []T 转换为 []interface

文章目录 前言官方解释内存布局程序运行中的内存布局通用方法 前言 在 Go 中&#xff0c;如果 interface{} 作为函数参数的话&#xff0c;是可以传任意参数的&#xff0c;然后通过类型断言来转换。 举个例子&#xff1a; package mainimport "fmt"func foo(v inter…...

两数相加 II——力扣445

题目描述 法一 栈 本题旨在从后往前加&#xff0c;为了逆序处理所有数位&#xff0c;利用栈&#xff0c;把数字压入栈中&#xff0c;再依次取出相加&#xff0c;注意进位&#xff01;进位是/10&#xff0c;另外需要注意栈的常用函数&#xff0c;push()、pop()、top()&#xff0…...

js获取上传视频的封面第一帧

代码如下&#xff1a;粘贴到这个在线编辑器里&#xff0c;可以测试效果。 菜鸟教程在线编辑器 <div><div style"flex: 1;border: 1px solid #999; position:relative;color: #333;background-color:#FFF2B8;"><span style"position: absolute…...

Nginx 高可用负载均衡(三种模式)

一、nginx普通集群负载均衡 1、安装keepalived (1)下载 https://www.keepalived.org/download.html(2)解压 tar -zxvf keepalived-2.0.18.tar.gz(3)使用configure命令配置安装目录与核心配置文件所在位置&#xff1a; ./configure --prefix/usr/local/keepalived --sysconf/e…...

Linux tail命令

在Linux中&#xff0c;tail命令用于查看文件的末尾内容。它可以显示文件的最后几行&#xff0c;默认情况下显示最后10行。 以下是一些常见的使用方式和示例&#xff1a; 显示文件的最后10行&#xff1a; tail filename将会显示名为filename的文件的最后10行内容。 显示文件…...

【屏幕适配发展介绍 Objective-C语言】

一、接下来,我们花一天时间,给大家介绍这个屏幕适配 1.那么,屏幕适配,是什么意思啊 我们说,写程序的时候,我们有时候要做 1)系统适配 2)屏幕适配 1)系统适配:是指的你写的这个代码,在iOS6、iOS7、iOS8,在不同的iOS系统下,是不是运行的效果,一致吧 这个指的是…...

linux中ls命令详解

ls 显示目录内容列表 补充说明 ls命令 就是list的缩写&#xff0c;用来显示目标列表&#xff0c;在Linux中是使用率较高的命令。ls命令的输出信息可以进行彩色加亮显示&#xff0c;以分区不同类型的文件。 语法 ls [选项] [文件名...][-1abcdfgiklmnopqrstuxABCDFGLNQRSUX…...

大盗阿福(记忆化搜索板子)

提供核心代码&#xff1a;&#xff08;经典的记忆化搜索套路&#xff09; int dfs(int pos){if(f[pos]!-1) return f[pos];//记忆化if(pos>n) return 0;//边界&#xff0c;越界int sum0;//模板int f10,f20;f1dfs(pos1);f2dfs(pos2)w[pos];summax(f1,f2);//模板f[pos]sum;//模…...

打卡力扣题目八

#左耳听风 ARST 打卡活动重启# 目录 一、问题 二、解题方法一 三、解题方法二 四、两种方法的区别 关于 ARTS 的释义 —— 每周完成一个 ARTS&#xff1a; ● Algorithm: 每周至少做一个 LeetCode 的算法题 ● Review: 阅读并点评至少一篇英文技术文章 ● Tips: 学习至少一…...

matlab使用教程(5)—矩阵定义和基本运算

本博客介绍如何在 MATLAB 中创建矩阵和执行基本矩阵计算。 MATLAB 环境使用矩阵来表示包含以二维网格排列的实数或复数的变量。更广泛而言&#xff0c;数组为向量、矩阵或更高维度的数值网格。MATLAB 中的所有数组都是矩形&#xff0c;在这种意义上沿任何维度的分量向量的长度…...

用HTML写一个简单的静态购物网站

实现代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>购物网站</title> &l…...

如何在go中实现程序的优雅退出,go-kratos源码解析

使用kratos这个框架有近一年了&#xff0c;最近了解了一下kratos关于程序优雅退出的具体实现。 这部分逻辑在app.go文件中&#xff0c;在main中&#xff0c;找到app.Run方法&#xff0c;点进入就可以了 它包含以下几个部分: App结构体:包含应用程序的配置选项和运行时状态。 …...

Appium+python自动化(二十八)- 高级滑动(超详解)

高级溜冰的滑动 滑动操作一般是两点之间的滑动&#xff0c;这种滑动在这里称其为低级的溜冰滑动&#xff1b;就是上一节给小伙伴们分享的。然而实际使用过程中用户可能要进行一些多点连续滑动操作。如九宫格滑动操作&#xff0c;连续拖动图片移动等场景。那么这种高级绚丽的溜…...

github token使用方法

git remote set-url origin https://<githubtoken>github.com/<username>/<repositoryname>.git 在私有仓库的HTTPS的url上加入<githubtoken>即为token url&#xff0c;可以免ssh key登录...

Spring属性注解对配置项名称的自动转换

一、前言 在Spring中&#xff0c;我们经常需要将配置文件中的属性值注入到Java类中。Spring提供了两个主要的注解来实现这一功能&#xff1a;Value 和 ConfigurationProperties。其中 ConfigurationProperties支持将配置项名称与Java类中的属性名进行自动转换&#xff0c;包括…...

Docker 安全 Docker HTTPS请求过程与配置

Docker 容器安全注意点 尽量别做的事 尽量不用 --privileged 运行容器&#xff08;授权容器root用户拥有宿主机的root权限&#xff09; 尽量不用 --network host 运行容器&#xff08;使用 host 网络模式共享宿主机的网络命名空间&#xff09; 尽量不在容器中运行 ssh 服务 尽…...

DevOps(三)

CD(二) 1. 整体流程2. 环境准备1. jenkins安装2. 编译安装git3. docker安装4. docker-compose安装5. sonarqube安装6. harbor安装7. gitlab私服8. maven安装9. Nexus部署10. K8s部署3. 安装java及编写代码3.1 安装java3.2 安装IntelliJ IDEA3.3 安装tomcat3.4 安装maven3.5 c…...

AOP的妙用

一、改代码 自定义注解用于提示该代码已经在AOP中重构了 public interface ReviseToAop {// 用于记录修改状态String value() default ""; }使用注解&#xff08;无意义&#xff0c;只是表名被修改&#xff09; ReviseToAop("修改于&#xff1a;2023/7/30&quo…...

CAN转ETHERCAT网关将CAN 总线和 ETHERCAT 网络连接方法

由于好多现场会出现将CAN总线的设备接到EtherCAT网络中&#xff0c;由于协议的不相同&#xff0c;不能直接进行连接&#xff0c;现需一种能同时兼容CAN 总线和ETHERCAT网络的一种设备&#xff0c;由此捷米JM-ECT-CAN 是自主研发的一款 ETHERCAT 从站功能的通讯网关。该产品主要…...

【大数据趋势】7月30日 汇率,恒指期货的大数据趋势概率分析。

1. 数据源头之一 : 汇率变化 从程序模拟趋势来看&#xff0c;美元在持续弱势状态&#xff0c;周线上正在构建一个新的下跌趋势&#xff0c;而且正在反抽过程中&#xff0c;即将完成&#xff0c;如果没有外部干预&#xff0c;会顺势往下。从月线来看&#xff0c;高点逐步降低&a…...

云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?

大家好&#xff0c;欢迎来到《云原生核心技术》系列的第七篇&#xff01; 在上一篇&#xff0c;我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在&#xff0c;我们就像一个拥有了一块崭新数字土地的农场主&#xff0c;是时…...

可靠性+灵活性:电力载波技术在楼宇自控中的核心价值

可靠性灵活性&#xff1a;电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中&#xff0c;电力载波技术&#xff08;PLC&#xff09;凭借其独特的优势&#xff0c;正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据&#xff0c;无需额外布…...

Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信

文章目录 Linux C语言网络编程详细入门教程&#xff1a;如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket&#xff08;服务端和客户端都要&#xff09;2. 绑定本地地址和端口&#x…...

【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)

本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...

快刀集(1): 一刀斩断视频片头广告

一刀流&#xff1a;用一个简单脚本&#xff0c;秒杀视频片头广告&#xff0c;还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农&#xff0c;平时写代码之余看看电影、补补片&#xff0c;是再正常不过的事。 电影嘛&#xff0c;要沉浸&#xff0c;…...

Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换

目录 关键点 技术实现1 技术实现2 摘要&#xff1a; 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式&#xff08;自动驾驶、人工驾驶、远程驾驶、主动安全&#xff09;&#xff0c;并通过实时消息推送更新车…...

【Kafka】Kafka从入门到实战:构建高吞吐量分布式消息系统

Kafka从入门到实战:构建高吞吐量分布式消息系统 一、Kafka概述 Apache Kafka是一个分布式流处理平台,最初由LinkedIn开发,后成为Apache顶级项目。它被设计用于高吞吐量、低延迟的消息处理,能够处理来自多个生产者的海量数据,并将这些数据实时传递给消费者。 Kafka核心特…...

Python常用模块:time、os、shutil与flask初探

一、Flask初探 & PyCharm终端配置 目的: 快速搭建小型Web服务器以提供数据。 工具: 第三方Web框架 Flask (需 pip install flask 安装)。 安装 Flask: 建议: 使用 PyCharm 内置的 Terminal (模拟命令行) 进行安装,避免频繁切换。 PyCharm Terminal 配置建议: 打开 Py…...

STM32标准库-ADC数模转换器

文章目录 一、ADC1.1简介1. 2逐次逼近型ADC1.3ADC框图1.4ADC基本结构1.4.1 信号 “上车点”&#xff1a;输入模块&#xff08;GPIO、温度、V_REFINT&#xff09;1.4.2 信号 “调度站”&#xff1a;多路开关1.4.3 信号 “加工厂”&#xff1a;ADC 转换器&#xff08;规则组 注入…...

用鸿蒙HarmonyOS5实现国际象棋小游戏的过程

下面是一个基于鸿蒙OS (HarmonyOS) 的国际象棋小游戏的完整实现代码&#xff0c;使用Java语言和鸿蒙的Ability框架。 1. 项目结构 /src/main/java/com/example/chess/├── MainAbilitySlice.java // 主界面逻辑├── ChessView.java // 游戏视图和逻辑├── …...