前言

作为一个 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]= 100fmt.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]= 100fmt.Printf("appendA %v\n", x)
}

在函数传递过程中,main 中的 x 与 appendA 函数中的 x 切片所引用的是同个数组。

所以在函数中对 x[0]=100main函数中也能获取到。

本质上修改的就是同一块内存数据。

值传递带来的误会

在上述例子中,在 appendB 中调用 append 函数追加数据后会发现 main 函数中并没有受到影响,这里我稍微调整了一下示例代码:

func TestAppendB(t *testing.T) {//x:=[]int{1,2,3}x := make([]int, 3,5)x[0] = 1x[1] = 2x[2] = 3appendB(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] = 1x[1] = 2x[2] = 3appendB(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] = 1x[1] = 2x[2] = 3appendB(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 就会发现这几个并没有什么联系,只是看起来很像容易唬人。

更多推荐内容

《Go 中的 channel 与 Java BlockingQueue 的本质区别

《写了一个 gorm 乐观锁插件

《一文搞懂参数传递原理

《撸了一个 Feign 增强包

《手写一个词法分析器

《手把手实现延时消息

如何参与一个顶级的开源项目

也许是东半球直接地气的分库分表实践了

《What?一个 Dubbo 服务启动要两个小时!

《又一次生产 CPU 高负载的排查实践》

《一次分表踩坑的探讨》

《一致性 Hash 算法的实际应用》

《利用策略模式优化过多 if else》

《长连接的心跳及重连设计》

《为自己搭建一个分布式 IM(即时通讯) 系统》

《一次生产 CPU 100% 排查优化实践》

《判断一个元素在亿级数据中是否不存在》

《设计一个可插拔的 IOC 容器》

《一次 HashSet 所引起的并发问题》

《一次内存溢出排查实践》

《如何优雅的使用和理解线程池》

[]*T *[]T *[]*T 傻傻分不清楚相关推荐

  1. JS魔法堂:属性、特性,傻傻分不清楚

    一.前言 或许你和我一样都曾经被下面的代码所困扰 var el = document.getElementById('dummy'); el.hello = "test"; con ...

  2. ASP.NET MVC涉及到的5个同步与异步,你是否傻傻分不清楚?[下篇]

    关于ASP.NET MVC对请求的处理方式(同步或者异步)涉及到的五个组件,在<上篇>中我们谈了三个(MvcHandler.Controller和ActionInvoker),现在我们来谈 ...

  3. 科普 | Shell中傻傻分不清楚的TOP3

    近来小姐姐又犯憨憨错误,问组内小伙伴export命令不会持久化环境变量吗?反正我是问出口了..然后小伙伴就甩给了我一个<The Linux Command Line>PDF链接.感谢老大不 ...

  4. 国家电网和南方电网还傻傻分不清?

    参看:都2020年了,国家电网和南方电网还傻傻分不清? 一.名称不同 一个叫南方电网,一个叫国家电力电网,虽然都是电网,但是区别还是很大的 而且成立时间不一样:国家电力电网有限公司成立于2002年12 ...

  5. cdn厂商 同兴万点_同兴万点:TXNetworks和CDNetworks让我们傻傻分不清

    原标题:同兴万点:TXNetworks和CDNetworks让我们傻傻分不清 在2008年2月25日成立的同兴万点,公司全称为同兴万点(北京)网络技术有限公司(TXNetworks),一直专注于CDN ...

  6. 2021年了,`IEnumerator`、`IEnumerable`接口还傻傻分不清楚?

    IEnumerator.IEnumerable这两个接口单词相近.含义相关,傻傻分不清楚. 入行多年,一直没有系统性梳理这对李逵李鬼. 最近本人在怼着why神的<其实吧,LRU也就那么回事> ...

  7. 动态ram依靠什么存储信息_ROM、RAM、DRAM、SRAM和FLASH傻傻分不清

    ROM.RAM.DRAM.SRAM和FLASH各类储存器在电脑.手机.电子设备.嵌入式设备及相应的开发中普遍应用的,但是很多还是傻傻分不清楚.下面就简单介绍下这几个吧! ROM和RAM ROM:只读存 ...

  8. Executor 与 ExecutorService 和 Executors 傻傻分不清

    转载自  Executor 与 ExecutorService 和 Executors 傻傻分不清 java.util.concurrent.Executor, java.util.concurren ...

  9. HashMap中傻傻分不清楚的那些概念

    转载自 HashMap中傻傻分不清楚的那些概念 很多人在通过阅读源码的方式学习Java,这是个很好的方式.而JDK的源码自然是首选.在JDK的众多类中,我觉得HashMap及其相关的类是设计的比较好的 ...

  10. 2运行内存多大_智能设备中的内存与容量为何傻傻分不清?它们的区别是什么?...

    在日常生活中,很多时候会把某些电子产品的容量说成内存,或者把内存说成了容量.比如有人问:"这个手机的内存多大?"或许会有这样回答的:"内存是256G."这种问答 ...

最新文章

  1. cctype 头文件定义函数实例
  2. 优秀的Java程序员必须了解GC的工作原理
  3. Java遗传算法并行多机调度程序
  4. 刚体Collider包围测试
  5. 别人的代码是火箭,我的代码是乌龟……
  6. QML资源加载和网络透明度
  7. 如何避免fstab挂载故障问题
  8. (二十六)【2021 WWW】Knowledge-Preserving Incremental Social Event Detection via Heterogeneous GNNs
  9. 常见25种深度学习模型的github代码
  10. RMAN报错:ORA-19573: 无法获得 exclusive 入队
  11. android+5+镜像,1 下载AOSP(Android)镜像
  12. unity3d ppsspp模拟器中的post processing shader在unity中使用
  13. unity打开htc vive 的前置摄像头 和 实现增强现实效果
  14. schema自动生成前端代码
  15. 使用数组大赛现场统分c语言,大奖赛现场统分-数组
  16. 旷视研究院「技术圆桌派-上海专场 」报名启动!
  17. 容联云(第三方短信发送)
  18. Jupyter Notebook简洁教程
  19. mysql 多表中间表查询_mysql多表连接查询
  20. ​LeetCode刷题实战371:两整数之和

热门文章

  1. 0.96寸OLED(SSD1306)屏幕显示(三)——屏幕垂直水平滚动
  2. Robot Framework自定义测试库
  3. 经典编程题——折纸问题
  4. C语言教你怎么改变字体颜色
  5. Excel2——在同一个图中如何绘制多条曲线并标注图例
  6. Linux内核4.14版本——DMA Engine框架分析(2)_功能介绍及解接口分析(slave client driver)
  7. 【每日最爱一句】2013.07.18
  8. 计算机科学方向的会议或期刊,计算机顶会和顶刊_计算机顶会_顶会
  9. 小米 VS 华为 - 抽象工厂模式
  10. Rose出现 “relation from A to B would cause an Invalid circular inheritance解决方法。