slice全解析

昨天组内小伙伴做分享,给出了这么一段代码:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func fun1(x int) {
  6. x = x + 1
  7. }
  8. func fun2(x *int) {
  9. *x = *x + 1
  10. }
  11. func fun3(x []int) {
  12. x = append(x, 3)
  13. }
  14. func fun4(x *[]int) {
  15. *x = append(*x, 3)
  16. }
  17. func fun5(x [4]int) {
  18. x[3] = 100
  19. }
  20. func fun6(x *[4]int) {
  21. (*x)[3] = 200
  22. }
  23. // 传值,传指针
  24. func main() {
  25. x1 := 10
  26. fmt.Printf("%+v\n", x1)
  27. fun1(x1)
  28. fmt.Printf("%+v\n\n", x1)
  29. fmt.Printf("%+v\n", x1)
  30. fun2(&x1)
  31. fmt.Printf("%+v\n\n", x1)
  32. var x3 []int
  33. x3 = append(x3, 0, 1, 2)
  34. fmt.Printf("%+v\n", x3)
  35. fun3(x3)
  36. fmt.Printf("%+v\n\n", x3)
  37. fmt.Printf("%+v\n", x3)
  38. fun4(&x3)
  39. fmt.Printf("%+v\n\n", x3)
  40. var x4 [4]int
  41. for i := 0; i < 4; i++ {
  42. x4[i] = i
  43. }
  44. fmt.Printf("%+v\n", x4)
  45. fun5(x4)
  46. fmt.Printf("%+v\n\n", x4)
  47. fmt.Printf("%+v\n", x4)
  48. fun6(&x4)
  49. fmt.Printf("%+v\n\n", x4)
  50. }

可以放在play上运行一下,实际输出的是

  1. 10
  2. 10
  3. 10
  4. 11
  5. [0 1 2]
  6. [0 1 2]
  7. [0 1 2]
  8. [0 1 2 3]
  9. [0 1 2 3]
  10. [0 1 2 3]
  11. [0 1 2 3]
  12. [0 1 2 200]

得出的结论是:slice是引用传递,数组是值传递,但是要想修改slice和数组,都需要把slice或者数组的地址传递进去。

这个结论中的数组是值传递,要在调用函数内部修改数组值,必须传递数组指针,我没有什么意见。但是slice的部分,却并没有那么简单。基本上,需要明确下面几点才能解释上面的代码。

slice的结构是uintptr+len+cap

比如我定义了一个slice, 不管是什么方法定义的

  1. var a []int
  2. a = make([]int, 1)
  3. a := []int{1,2}

这里的a都是由一个固定的数据结构赋值的

这个数据结构有三个,一个是指向一个定长数组的指针,一个是len,表示我这个slice包含了几个值,还有一个是cap,表示我申请定长数组的时候申请了多大的空间。

slice的append操作是根据cap和len的关系判断是否申请新的空间

在内存看空间中,没有不定长的数组,所有不定长数组的语法都是语言本身封装了。比如golang中的slice。slice可以在初始化的时候就定义好我需要使用多大的空间(cap)

  1. a := make([]int, 1, 10)

这里的10也就是cap,1是len,说明我已经创建了10个int空间给这个slice。

当不断往a中append数据的时候,首先是len不断增加,当len和cap一样的时候,这个时候再append数据,就会新开辟一个数组空间,这个数组空间长度为多大呢?2*cap。

举例说,如果上述的a后续又append了9个数据,这个时候如果再append一个数据,就会发现cap变成20了。

当然,如果扩容了,那么我们说的slice的第一个元素,指向定长数组的地址就会变化。

理解下下面这个代码:

  1. package main
  2. import "fmt"
  3. import "unsafe"
  4. func main() {
  5. var a []int
  6. a = append(a, 0)
  7. printSlice("a", a)
  8. a = append(a, 1)
  9. printSlice("a", a)
  10. a = append(a, 2, 3, 4)
  11. printSlice("a", a)
  12. }
  13. func printSlice(s string, x []int) {
  14. fmt.Printf("%s len=%d cap=%d ptr=%p %v\n",s, len(x), cap(x), unsafe.Pointer(&x[0]), x)
  15. }
  16. 输出:
  17. a len=1 cap=2 ptr=0x10414020 [0]
  18. a len=2 cap=2 ptr=0x10414020 [0 1]
  19. a len=5 cap=8 ptr=0x10458020 [0 1 2 3 4]

函数参数是slice的时候传递的是“slice结构”的值拷贝

我们说的slice为参数传递的时候传递是引用传递,实际上,它传递的是Slice结构(uintptr+len+cap)的一个复制,但是由于uintptr对应的是一个定长的数组,所以基本上当slice作为参数传递的时候,返回回来的slice结构是不会变的,对应的定长数组的大小是不会变的,但是这个定长数组里面的具体值是有可能变的。

看下面几个例子:

  1. package main
  2. import (
  3. "fmt"
  4. "unsafe"
  5. )
  6. func fun3(x []int) {
  7. fmt.Printf("%p\n", unsafe.Pointer(&x[0]))
  8. x = append(x, 3)
  9. x[2] = 100
  10. fmt.Printf("%p\n", unsafe.Pointer(&x[0]))
  11. }
  12. func main() {
  13. var x3 []int
  14. x3 = append(x3, 0, 1, 2)
  15. fmt.Printf("%+v\n", x3)
  16. fmt.Printf("%p\n", unsafe.Pointer(&x3[0]))
  17. fun3(x3)
  18. fmt.Printf("%p\n", unsafe.Pointer(&x3[0]))
  19. fmt.Printf("%+v\n\n", x3)
  20. }
  21. 输出:
  22. [0 1 2]
  23. 0x10414020
  24. 0x10414020
  25. 0x10414020
  26. 0x10414020
  27. [0 1 100]

这里的x3[2]的的值变化了。但是slice的指针地址没有变化。

如果在f3里面修改x[3]的值:

  1. package main
  2. import (
  3. "fmt"
  4. "unsafe"
  5. )
  6. func fun3(x []int) {
  7. fmt.Printf("%p\n", unsafe.Pointer(&x[0]))
  8. x = append(x, 3)
  9. x[3] = 100
  10. fmt.Printf("%p\n", unsafe.Pointer(&x[0]))
  11. }
  12. func main() {
  13. var x3 []int
  14. x3 = append(x3, 0, 1, 2)
  15. fmt.Printf("%+v\n", x3)
  16. fmt.Printf("%p\n", unsafe.Pointer(&x3[0]))
  17. fun3(x3)
  18. fmt.Printf("%p\n", unsafe.Pointer(&x3[0]))
  19. fmt.Printf("%+v\n\n", x3)
  20. }
  21. 输出:
  22. [0 1 2]
  23. 0x10414020
  24. 0x10414020
  25. 0x10414020
  26. 0x10414020
  27. [0 1 2]

这里的x3的值就不会变化,虽然不会变化,但是实际上slice指向的定长数组的索引为3的值已经变化了。

如果f3是append两个呢?

  1. package main
  2. import (
  3. "fmt"
  4. "unsafe"
  5. )
  6. func fun3(x []int) {
  7. fmt.Printf("%p\n", unsafe.Pointer(&x[0]))
  8. x = append(x, 3, 4)
  9. x[3] = 100
  10. fmt.Printf("%p\n", unsafe.Pointer(&x[0]))
  11. }
  12. func main() {
  13. var x3 []int
  14. x3 = append(x3, 0, 1, 2)
  15. fmt.Printf("%+v\n", x3)
  16. fmt.Printf("%p\n", unsafe.Pointer(&x3[0]))
  17. fun3(x3)
  18. fmt.Printf("%p\n", unsafe.Pointer(&x3[0]))
  19. fmt.Printf("%+v\n\n", x3)
  20. }
  21. 输出:
  22. [0 1 2]
  23. 0x10414020
  24. 0x10414020
  25. 0x10458000
  26. 0x10414020
  27. [0 1 2]

我们看到在fun3里面append之后的x这个slice指向的地址变化了。但是由于这个x实际上是我们传递进去的x3的值拷贝,所以这个x3并没有被修改。最后输出的时候还是没有变化。

总结

基本上记住了这几个点就明白了slice:

  • slice的结构是uintptr+len+cap
  • slice的append操作是根据cap和len的关系判断是否申请新的空间
  • 函数参数是slice的时候传递的是“slice结构”的值拷贝

参考

https://halfrost.com/go_slice/

slice全解析的更多相关文章

  1. Go切片全解析

    Go切片全解析 目录结构: 数组 切片 底层结构 创建 普通声明 make方式 截取 边界问题 追加 拓展表达式 扩容机制 切片传递的坑 切片的拷贝 浅拷贝 深拷贝 数组 var n [4]int f ...

  2. Google Maps地图投影全解析(3):WKT形式表示

    update20090601:EPSG对该投影的编号设定为EPSG:3857,对应的WKT也发生了变化,下文不再修改,相对来说格式都是那样,可以到http://www.epsg-registry.or ...

  3. C#系统缓存全解析(转载)

    C#系统缓存全解析 对各种缓存的应用场景和方法做了很详尽的解读,这里推荐一下 转载地址:http://blog.csdn.net/wyxhd2008/article/details/8076105

  4. 【凯子哥带你学Framework】Activity界面显示全解析

    前几天凯子哥写的Framework层的解析文章<Activity启动过程全解析>,反响还不错,这说明“写让大家都能看懂的Framework解析文章”的思想是基本正确的. 我个人觉得,深入分 ...

  5. iOS Storyboard全解析

    来源:http://iaiai.iteye.com/blog/1493956 Storyboard)是一个能够节省你很多设计手机App界面时间的新特性,下面,为了简明的说明Storyboard的效果, ...

  6. 【转载】Fragment 全解析(1):那些年踩过的坑

    http://www.jianshu.com/p/d9143a92ad94 Fragment系列文章:1.Fragment全解析系列(一):那些年踩过的坑2.Fragment全解析系列(二):正确的使 ...

  7. (转)ASP.NET缓存全解析6:数据库缓存依赖

    ASP.NET缓存全解析文章索引 ASP.NET缓存全解析1:缓存的概述 ASP.NET缓存全解析2:页面输出缓存 ASP.NET缓存全解析3:页面局部缓存 ASP.NET缓存全解析4:应用程序数据缓 ...

  8. jQuery&nbsp;Ajax&nbsp;实例&nbsp;全解析

    jQuery Ajax 实例 全解析 jQuery确实是一个挺好的轻量级的JS框架,能帮助我们快速的开发JS应用,并在一定程度上改变了我们写JavaScript代码的习惯. 废话少说,直接进入正题,我 ...

  9. ARM内核全解析,从ARM7,ARM9到Cortex-A7,A8,A9,A12,A15到Cortex-A53,A57

    转自: ARM内核全解析,从ARM7,ARM9到Cortex-A7,A8,A9,A12,A15到Cortex-A53,A57 前不久ARM正式宣布推出新款ARMv8架构的Cortex-A50处理器系列 ...

随机推荐

  1. Extjs6.2 项目学习系列(一)

     1. Extjs开发准备  (1).下载ExtJS  SDK GPL版 (本测试用版本 ext-6.2.0) : https://www.sencha.com/legal/gpl/ (2).下载Se ...

  2. python url参数转dict

    from urllib import parse url='_from=R40&LH_BIN=1&_sop=13&LH_Complete=1&LH_Sold=1& ...

  3. JS精度问题(0.1+0.2 = 0.3吗?)

    一.引出问题 0.1+0.2 = 0.3吗?在JS中是这样的吗?我们写个测试代码不就知道了吗? 结果出人意料,并不像我们所想象的那样.那么这到底是为什么呢? 二.原因分析 JS浮点数存储机制: 三.解 ...

  4. HTTP协议头部与Keep-Alive模式详解(转)

    转自:http://a280606790.iteye.com/blog/1095085 http1.1 中怎么打开持久连接,怎么关闭,怎么传输数据(确定本次数据是否传输完毕) 1.什么是Keep-Al ...

  5. DEDECMS织梦文章摘要批量更改方法

    我们建站有时候需要直接把数据库导入,只要修改一下基本的名称信息就可以直接用,但是遇用到一些问题.比如文章摘要不会随着文章内容的更新而更新.织梦(dede)在添加文章的时候会自动生成文章摘要,如果重新修 ...

  6. 微信小程序之微信登陆 —— 微信小程序教程系列(20)

    简介: 微信登陆,在新建一个微信小程序Hello World项目的时候,就可以看到项目中出现了我们的微信头像,其实这个Hello World项目,就有一个简化版的微信登陆.只不过是,还没有写入到咱们自 ...

  7. C++进阶:新人易入的那些坑 --1.常量、常指针和指针常量

    声明:以下内容B站/Youtube学习笔记,https://www.youtube.com/user/BoQianTheProgrammer/ Advanced C++. /* why use con ...

  8. 网页加水印 svg 方式

    /** *网页加水印 svg 方式 * * @export * @param {*} [{ * container = document.body, * content = '请勿外传', * wid ...

  9. SpringBoot几种定时任务的实现方式

    定时任务实现的几种方式: Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务.使用这种方式可以让你的程序按照某一个频度执行, ...

  10. Linux yun命令使用报错:File "/usr/bin/yum", line 30 except KeyboardInterrupt, e:

    原文参考:https://www.cnblogs.com/caiji/p/7891923.html 使用yum更新perl源,报错 问题出现原因: yum包管理是使用python2.x写的,将pyth ...