[]*T *[]T *[]*T 傻傻分不清楚

前言
作为一个 Go 语言新手,看到一切”诡异“的代码都会感到好奇;比如我最近看到的几个方法;伪代码如下:
func FindA() ([]*T,error) {
}
func FindB() ([]T,error) {
}
func SaveA(data *[]T) error {
}
func SaveB(data *[]*T) error {
}
相信大部分刚入门 Go 的新手看到这样的代码也是一脸懵逼,其中最让人疑惑的就是:
[]*T
*[]T
*[]*T
这样对切片的声明,先不看后面两种写法;单独看 []*T 还是很好理解的:
该切片中存放的是所有 T 的内存地址,会比存放 T 本身来说要更省空间,同时 []*T 在方法内部是可以修改 T 的值,而[]T 是修改不了。
func TestSaveSlice(t *testing.T) {
	a := []T{{Name: "1"}, {Name: "2"}}
	for _, t2 := range a {
		fmt.Println(t2)
	}
	_ = SaveB(a)
	for _, t2 := range a {
		fmt.Println(t2)
	}
}
func SaveB(data []T) error {
	t := data[0]
	t.Name = "1233"
	return nil
}
type T struct {
	Name string
}
比如以上例子打印的是
{1}
{2}
{1}
{2}
只有将方法修改为
func SaveB(data []*T) error {
	t := data[0]
	t.Name = "1233"
	return nil
}
才能修改 T 的值:
&{1}
&{2}
&{1233}
&{2}
示例
下面重点来看看 []*T 与 *[]T 的区别,这里写了两个 append 函数:
func TestAppendA(t *testing.T) {
	x:=[]int{1,2,3}
	appendA(x)
	fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
	x[0]= 100
	fmt.Printf("appendA %v\n", x)
}
先看第一种,输出是结果是:
appendA [1000 2 3]
main [1000 2 3]
说明在函数传递过程中,函数内部的修改能够影响到外部。
下面我们再看一个例子:
func appendB(x []int) {
	x = append(x, 4)
	fmt.Printf("appendA %v\n", x)
}
最终结果却是:
appendA [1 2 3 4]
main [1 2 3]
没有影响到外部。
而当我们再调整一下会发现又有所不同:
func TestAppendC(t *testing.T) {
	x:=[]int{1,2,3}
	appendC(&x)
	fmt.Printf("main %v\n", x)
}
func appendC(x *[]int) {
	*x = append(*x, 4)
	fmt.Printf("appendA %v\n", x)
}
最终的结果:
appendA &[1 2 3 4]
main [1 2 3 4]
可以发现如果传递切片的指针时,使用 append 函数追加数据时会影响到外部。
slice 原理
在分析上面三种情况之前,我们先来了解下 slice 的数据结构。
直接查看源码会发现 slice 其实就是一个结构体,只是不能直接对外访问。

源码地址
runtime/slice.go
其中有三个重要的属性:
| 属性 | 含义 | 
|---|---|
| array | 底层存放数据的数组,是一个指针。 | 
| len | 切片长度 | 
| cap | 切片容量 cap>=len | 
提到切片就不得不想到数组,可以这么理解:
切片是对数组的抽象,而数组则是切片的底层实现。
其实通过切片这个名字也不难看出,它就是从数组中切了一部分;相对于数组的固定大小,切片可以根据实际使用情况进行扩容。
所以切片也可以通过对数组"切一刀"获得:
x1:=[6]int{0,1,2,3,4,5}
x2 := x[1:4]
fmt.Println(len(x2), cap(x2))

其中 x1 的长度与容量都是6。
x2 的长度与容量则为3和5。
- x2 的长度很容易理解。
- 容量等于5可以理解为,当前这个切片最多可以使用的长度。
因为切片 x2 是对数组 x1 的引用,所以底层数组排除掉左边一个没有被引用的位置则是该切片最大的容量,也就是5。
同一个底层数组
以刚才的代码为例:
func TestAppendA(t *testing.T) {
	x:=[]int{1,2,3}
	appendA(x)
	fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
	x[0]= 100
	fmt.Printf("appendA %v\n", x)
}

在函数传递过程中,main 中的 x 与 appendA 函数中的 x 切片所引用的是同个数组。
所以在函数中对 x[0]=100,main函数中也能获取到。

本质上修改的就是同一块内存数据。
值传递带来的误会
在上述例子中,在 appendB 中调用 append 函数追加数据后会发现 main 函数中并没有受到影响,这里我稍微调整了一下示例代码:
func TestAppendB(t *testing.T) {
	//x:=[]int{1,2,3}
	x := make([]int, 3,5)
	x[0] = 1
	x[1] = 2
	x[2] = 3
	appendB(x)
	fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendB(x []int) {
	x = append(x, 444)
	fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}
主要是修改了切片初始化方式,使得容量大于了长度,具体原因后续会说明。
输出结果如下:
appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5
main 函数中的数据看样子确实没有受到影响;但细心的朋友应该会注意到  appendB 函数中的 x 在 append() 之后长度 +1 变为了4。
而在 main 函数中长度又变回了3.
这个细节区别就是为什么 append() "看似" 没有生效的原因;至于为什么要说“看似”,再次调整了代码:
func TestAppendB(t *testing.T) {
	//x:=[]int{1,2,3}
	x := make([]int, 3,5)
	x[0] = 1
	x[1] = 2
	x[2] = 3
	appendB(x)
	fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
	y:=x[0:cap(x)]
	fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}
在刚才的基础之上,以 append 之后的 x 为基础再做了一个切片;该切片的范围为 x 所引用数组的全部数据。
再来看看执行结果如何:
appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5
y [1 2 3 444 0] len=5,cap=5
会神奇的发现 y 将所有数据都打印出来,在 appendB 函数中追加的数据其实已经写入了数组中,但为什么 x 本身没有获取到呢?

看图就很容易理解了:
- 在appendB中确实是对原始数组追加了数据,同时长度也增加了。
- 但由于是值传递,所以 slice这个结构体即便是修改了长度为4,也只是对复制的那个对象修改了长度,main中的长度依然为3.
- 由于底层数组是同一个,所以基于这个底层数组重新生成了一个完整长度的切片便能看到追加的数据了。
所以这里本质的原因是因为 slice 是一个结构体,传递的是值,不管方法里如何修改长度也不会影响到原有的数据(这里指的是长度和容量这两个属性)。
切片扩容
还有一个需要注意:
刚才特意提到这里的例子稍有改变,主要是将切片的容量设置超过了数组的长度;
如果不做这个特殊设置会怎么样呢?
func TestAppendB(t *testing.T) {
	x:=[]int{1,2,3}
	//x := make([]int, 3,5)
	x[0] = 1
	x[1] = 2
	x[2] = 3
	appendB(x)
	fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
	y:=x[0:cap(x)]
	fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}
func appendB(x []int) {
	x = append(x, 444)
	fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}
输出结果:
appendB [1 2 3 444] len=4,cap=6
main [1 2 3] len=3,cap=3
y [1 2 3] len=3,cap=3
这时会发现 main 函数中的 y 切片数据也没有发生变化,这是为什么呢?

这是因为初始化 x 切片时长度和容量都为3,当在 appendB 函数中追加数据时,会发现没有位置了。
这时便会进行扩容:
- 将老数据复制一份到新的数组中。
- 追加数据。
- 将新的数据内存地址返回给 appendB中的 x .
同样的由于是值传递,所以 appendB 中的切片换了底层数组对 main 函数中的切片没有任何影响,也就导致最终 main 函数的数据没有任何变化了。
传递切片指针
有没有什么办法即便是在扩容时也能对外部产生影响呢?
func TestAppendC(t *testing.T) {
	x:=[]int{1,2,3}
	appendC(&x)
	fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendC(x *[]int) {
	*x = append(*x, 4)
	fmt.Printf("appendC %v\n", x)
}
输出结果为:
appendC &[1 2 3 4]
main [1 2 3 4] len=4,cap=6
这时外部的切片就能受到影响了,其实原因也很简单;
刚才也说了,因为 slice 本身是一个结构体,所以当我们传递指针时,就和平时自定义的 struct 在函数内部通过指针修改数据原理相同。
最终在 appendC 中的 x 的指针指向了扩容后的结构体,因为传递的是 main 函数中 x 的指针,所以同样的 main 函数中的 x 也指向了该结构体。
总结
所以总结一下:
- 切片是对数组的抽象,同时切片本身也是一个结构体。
- 参数传递时函数内部与外部引用的是同一个数组,所以对切片的修改会影响到函数外部。
- 如果发生扩容,情况会发生变化,同时扩容会导致数据拷贝;所以要尽量预估切片大小,避免数据拷贝。
- 对切片或数组重新生成切片时,由于共享的是同一个底层数组,所以数据会互相影响,这点需要注意。
- 切片也可以传递指针,但场景很少,还会带来不必要的误解;建议值传值就好,长度和容量占用不了多少内存。
相信使用过切片会发现非常类似于  Java  中的 ArrayList,同样是基于数组实现,也会扩容发生数据拷贝;这样看来语言只是上层使用的选择,一些通用的底层实现大家都差不多。
这时我们再看标题中的 []*T *[]T *[]*T 就会发现这几个并没有什么联系,只是看起来很像容易唬人。
[]*T *[]T *[]*T 傻傻分不清楚的更多相关文章
- JS魔法堂:属性、特性,傻傻分不清楚
		一.前言 或许你和我一样都曾经被下面的代码所困扰 var el = document.getElementById('dummy'); el.hello = "test"; con ... 
- MVP  MVC  MVVM  傻傻分不清
		最近MVC (Model-View-Controller) 和MVVM (Model-View-ViewModel) 在微软圈成为显学,ASP.NET MVC 和WPF 的Prism (MVVM Fr ... 
- Java:接口和抽象类,傻傻分不清楚?
		01. 来看网络上对接口的一番解释: 接口(英文:Interface),在 Java 编程语言中是一个抽象类型,是抽象方法的集合.一个类通过继承接口的方式,从而来继承接口的抽象方法. 兄弟们,你们怎么 ... 
- [转帖]十分钟快速理解DPI和PPI,不再傻傻分不清!
		十分钟快速理解DPI和PPI,不再傻傻分不清! https://baijiahao.baidu.com/s?id=1605834796518990333&wfr=spider&for= ... 
- OCA,OCP,OCM傻傻分不清?
		可能大家知道OCA.OCP.OCM的关系是一个比一个难考,一个比一个含金量高,但是你知道具体的考试科目.考试方式.就业形势区别吗?不知道的话这篇通俗易懂的文章会让你一目了然. 区别一:含金量 ■OCA ... 
- 学点经济学:M0、M1、M2、M3,傻傻分不清?(转载)
		来源:http://t.10jqka.com.cn/pid_97006727.shtml 学点经济学:M0.M1.M2.M3,傻傻分不清? 25,508人浏览 2018-08-03 11:06 常听人 ... 
- 【华为敏捷/DevOps实践】7. 敏捷,DevOps,傻傻不分清楚【华为云技术分享】
		文:姚冬(华为云DevCloud首席技术布道师,资深DevOps与精益/敏捷专家,金融解决方案技术Leader,中国DevOpsDays社区核心组织者) 前言 敏捷是什么?DevOps是什么?两者有什 ... 
- 傻傻分不清之 Cookie、Session、Token、JWT
		傻傻分不清之 Cookie.Session.Token.JWT 什么是认证(Authentication) 通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打 ... 
- Shell中傻傻分不清楚的TOP3
		Shell中傻傻分不清楚的TOP3 发布文章 近来小姐姐又犯憨憨错误,问组内小伙伴export命令不会持久化环境变量吗?反正我是问出口了..然后小伙伴就甩给了我一个<The Linux Comm ... 
- ASCII、Unicode、UTF-8、UTF-8(without BOM)、UTF-16、UTF-32傻傻分不清
		ASCII.Unicode.UTF-8.UTF-8(without BOM).UTF-16.UTF-32傻傻分不清 目录 ASCII.Unicode.UTF-8.UTF-8(without BOM). ... 
随机推荐
- 数据泵导入,报错:ORA-12899: value too large for column "SCOTT"."TEST112"."JOIN" (actual: 9, maximum: 8)
			1.报错: 数据泵执行导入时报错:ORA-12899: value too large for column "SCOTT"."TEST112"."J ... 
- Spring事件发布与监听机制
			我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的JavaLib」第一时间阅读最新文章,回复[资料],即可获得我精心整理的技术资料,电子书籍,一线大厂面试资料和优秀简历模板. 目录 ... 
- js关于数组的操作(合并数组、添加数组、循环等)
			1. concat() 方法 concat() 方法用于连接两个或多个数组 var arr = new Array(3) arr[0] = "George" arr[1] = &q ... 
- sql循环说明
			while循环:主要是判断,不能使用表中的ID,临时表是ID自增的,通过自增ID可以查出表ID(语法简单,需要配合其他代码操作表ID)游标循环:可以使用表中的ID ,进行修改等操作(语法难一点,核心代 ... 
- Redis配置统计字典
			本章将对Redis的系统状态信息(info命令结果)和Redis的所有配置(包括Standalone.Sentinel.Cluster三种模式)做一个全面的梳理,希望本章能够成为Redis配置统计字典 ... 
- Netty 框架学习 —— 添加 WebSocket 支持
			WebSocket 简介 WebSocket 协议是完全重新设计的协议,旨在为 Web 上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息 Netty 对于 ... 
- Opencv 播放mp4文件和读取摄像头图以及可能会发生的一些异常问题解决方法
			学习内容 学习Opencv 读取并播放本地视频和打开摄像头图像以及可能会发生的一些异常问题解决方法 代码演示 电脑环境信息: OpenCV版本:4.5.2 ,vs2017 1.视频文件读取与播放 加载 ... 
- 14、iptables_nat源地址转换(内网共享上网)
			14.1.环境说明: 1.架构图: 
- 18、mysql读写分离实现的方法
			18.1.mysql读写分离实现的方法: 1.通过程序实现读写分离: php和java程序实现读写分离(性能,效率最佳,推荐); php和java程序都可以通过设置多个连接文件轻松实现对数据库的读写分 ... 
- 『无为则无心』Python序列 — 22、Python集合及其常用操作
			目录 1.Python集合特点 2.Python集合的创建 3.操作集合常用API (1)增加数据 @1.add()方法 @2.update()方法 (2)删除数据 @1.remove()方法 @2. ... 
