首先我们来看段代码的输出

    s := make([][]int, 4)for i := 0; i < 4; i++ {s[i] = make([]int, 4)s[i][0] = 1}s0 := s[0]s0 = append(s0, 5)fmt.Println(s)

输出的结果是

[[1 0 0 0] [1 0 0 0] [1 0 0 0] [1 0 0 0]]

append的值5并没有输出,那么究竟是s0并不等价于s[0],还是有其他原因呢?
首先,肯定的是在Go中,所有的拷贝都是值拷贝,不存在引用拷贝;
其次,slice由一个指针,两个整型(分别是size和cap)来实现,既然s0包含的指针和s[0]包含的指针相同,为什么append操作并没有达到预期效果呢?

带着这些疑问,我们来看看slice的底层实现。

slice的结构

type slice struct {array unsafe.Pointerlen   intcap   int
}

array指向一个数组元素的地址,这个数组可能在makeslice时创建,也可能之前就存在,而slice被"attach"上去,例如 s := a[0:5];

//代码比较简单
func makeslice(et *_type, len, cap int) slice {maxElements := maxSliceCap(et.size)if len < 0 || uintptr(len) > maxElements {panic(errorString("makeslice: len out of range"))}if cap < len || uintptr(cap) > maxElements {panic(errorString("makeslice: cap out of range"))}//分配数组空间p := mallocgc(et.size*uintptr(cap), et, true)return slice{p, len, cap}
}

e.g.

s := make([]int, 1, 3)
fmt.Printf("%p, %v, len:%d, cap:%d", unsafe.Pointer(&s[0]), s, len(s), cap(s))

输出:

0xc42007c0a0, [0], len:1, cap:3

对于直接"attach"到数组的情形,类似下面这样

a := [10]int{0,1,2,3,4,5,6,7,8,9}fmt.Printf("%p, %v \n", &a, a)s1 := a[1:5:7]fmt.Printf("%p, %v, len:%d, cap:%d, self:%p \n", unsafe.Pointer(&s1[0]), s1, len(s1), cap(s1), unsafe.Pointer(&s1) )

输出:

0xc42006e050, [0 1 2 3 4 5 6 7 8 9]
0xc42006e058, [1 2 3 4], len:4, cap:6, self:0xc42006c140

根据两个指针的关系,可以看出,slice直接指向了数组中的元素

slice attach1

同理,slice2还可以通过另一个slice1构造,但其属性依赖slice1,并不是slice1底层的数组

s2 := s1[2:]
fmt.Printf("%p, %v, len:%d, cap:%d, self:%p \n", unsafe.Pointer(&s2[0]), s2, len(s2), cap(s2), unsafe.Pointer(&s2) )

输出

0xc42006e068, [3 4], len:2, cap:4, self:0xc42006a140

slice attach2

修改slice中元素的值,实际上修改的是底层数组元素的值

s2[0] = 100
fmt.Println(a, s1, s2)

输出

[0 1 2 100 4 5 6 7 8 9] [1 2 100 4] [100 4]

扩张

当我们对slice进行append操作时,若len + 追加元素个数 <= cap时,不会发生内存扩张;否则,新的内存被申请,同时旧的数据被拷贝至新内存的前部;

先上代码

func growslice(et *_type, old slice, cap int) slice {......if et.size == 0 {if cap < old.cap {panic(errorString("growslice: cap out of range"))}return slice{unsafe.Pointer(&zerobase), old.len, cap}}// 计算新的cap,针对不同情况分别处理newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {if old.len < 1024 {//2倍newcap = doublecap} else {//每次尝试扩1/4for newcap < cap {newcap += newcap / 4}}}var lenmem, newlenmem, capmem uintptrconst ptrSize = unsafe.Sizeof((*byte)(nil))switch et.size {case 1:lenmem = uintptr(old.len)newlenmem = uintptr(cap)capmem = roundupsize(uintptr(newcap))newcap = int(capmem)case ptrSize:lenmem = uintptr(old.len) * ptrSizenewlenmem = uintptr(cap) * ptrSizecapmem = roundupsize(uintptr(newcap) * ptrSize)newcap = int(capmem / ptrSize)default:lenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem = roundupsize(uintptr(newcap) * et.size)newcap = int(capmem / et.size)}......var p unsafe.Pointerif et.kind&kindNoPointers != 0 {//这里有个优化细节,先不zero,因为前部会发生覆盖p = mallocgc(capmem, nil, false)memmove(p, old.array, lenmem)//只对后部zeromemclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)} else {p = mallocgc(capmem, et, true)if !writeBarrier.enabled {memmove(p, old.array, lenmem)} else {for i := uintptr(0); i < lenmem; i += et.size {typedmemmove(et, add(p, i), add(old.array, i))}}}return slice{p, old.len, newcap}
}

当不需要扩容时,append会修改底层数组的值

s2 = append(s2, 1000)
fmt.Printf("%p, %v \n", unsafe.Pointer(&s2[0]), s2)
fmt.Println(a, s1, s2)
fmt.Printf("len(s1)=%d, cap(s1)=%d, len(s2)=%d, cap(s2)=%d", len(s1), cap(s1), len(s2), cap(s2))

输出

[0 1 2 100 4 1000 6 7 8 9] [1 2 100 4] [100 4 1000]
len(s1)=4, cap(s1)=6, len(s2)=3, cap(s2)=4

注意,对s2的append,仅改变了s2的len,并没有改变s1的len
当append过多时,slice底层数组会发生变化

s2 = append(s2, 1000, 1001, 1002)
fmt.Printf("%p, %v \n", unsafe.Pointer(&s2[0]), s2)
fmt.Println(a, s1, s2)
fmt.Printf("len(s1)=%d, cap(s1)=%d, len(s2)=%d, cap(s2)=%d", len(s1), cap(s1), len(s2), cap(s2))

输出

0xc420014280, [100 4 1000 1001 1002]
[0 1 2 100 4 5 6 7 8 9] [1 2 100 4] [100 4 1000 1001 1002]
len(s1)=4, cap(s1)=6, len(s2)=5, cap(s2)=8

原数组a,切片s1的属性未受影响;但s2底层的数组已发生变化,cap也是之前的2倍。

总结

1、多个slice指向相同的底层数组时,修改其中一个slice,可能会影响其他slice的值;
2、slice作为参数传递时,比数组更为高效,因为slice的结构比较小;
3、slice在扩张时,可能会发生底层数组的变更及内存拷贝;
4、因为slice引用了数组,这可能导致数组空间不会被gc,当数组空间很大,而slice引用内容很少时尤为严重;

作者:Love语鬼
链接:https://www.jianshu.com/p/f268bebc722f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Golang slice 的底层实现相关推荐

  1. GoLang之interface底层系列二(类型断言)

    文章目录 GoLang之interface底层系列二(类型断言) 1.抽象类型.具体类型 2.断言的作用类型与目标类型 3.空接口.(具体类型) 4.非空接口.(具体类型) 5.空接口.(非空接口) ...

  2. golang slice map扩容

    golang slice 扩容 操作系统预分配的内存规格 byte 8 16 32 64 80 96 112- 先求出当前切片容量x,求出append追加后的容量 y 判断 x*2 和y 的关系 1 ...

  3. Golang Slice切片如何扩容

    Golang Slice切片如何扩容 Golang轻松学习 文章目录 Golang Slice切片如何扩容 一.Slice数据结构是什么? 二.详细代码 1.数据结构 2.扩容原则 2.如何理解扩容规 ...

  4. GoLang之map底层系列二(浅显学习)

    文章目录 GoLang之map底层系列二(浅显学习) 1.map++ 2.map引用传递 3.map不是并发安全 4.map的value为空接口 5.map的value为切片 6.value,ok=m ...

  5. GoLang之channel底层的数据结构是什么、channel的创建(2)

    文章目录 GoLang之channel底层的数据结构是什么.channel的创建(2) 1.数据结构 2.创建 GoLang之channel底层的数据结构是什么.channel的创建(2) 1.数据结 ...

  6. Golang slice原理

    数组 slice类型是建立在Go数组类型之上的抽象,因此要了解slice我们必须首先了解数组. 数组类型定义了长度和元素类型.例如,[4]int 类型表示一个由四个整数组成的数组.数组的大小是固定的, ...

  7. golang slice扩容机制

    Slice expanse capacity slice这种数据结构便于使用和管理数据集合,可以理解为是一种动态数组,slice也是围绕动态数组的概念来构建的.既然是动态数组,那么slice是如何扩容 ...

  8. Golang之接口底层分析

    目录 GoLang之iface 和 eface 的区别是什么? GoLang之接口的动态类型和动态值 [引申1]接口类型和 `nil` 作比较 [引申2] [引申3]如何打印出接口的动态类型和值? G ...

  9. [Golang]slice值传递存在的问题

    说明 因为slice是指针类型,所以很多人会误以为其传递的是引用,所以在使用的过程中会出现一些非期望的实现. 问题 在使用append函数时,比如以下函数: func sliceModify(slic ...

最新文章

  1. 标题 相机标定(Camera calibration)原理和步骤
  2. Java 线程 知识
  3. 和dump文件_SRA数据库及下载二代测序原始数据转换为fastq文件
  4. python简单代码加法-Python tkinter实现简单加法计算器代码实例
  5. Kubernetes-Ingress(十九)
  6. leetcode力扣36.有效的数独
  7. [RL] pip 安装 atari-py
  8. 如何从技术上成功预测比特币价格?
  9. TSF自定义候选词列表界面
  10. 配置FreeSWITCH支持不带媒体信息的SIP信令
  11. 全面解读流程图|附共享单车摩拜ofo案例分析
  12. 我的新书《C++服务器开发精髓》终于出版啦
  13. c 连接oracle otl,C++类库:OTL通用的数据库连接类库
  14. 计算机常用英语词汇及读音,程序员相关常见英文单词的正确读法
  15. 带登录页面的猜数字小游戏
  16. 继承(下)----虚继承
  17. CET-4 week7 融会贯通
  18. linux 开机画面
  19. IT十年人生过客-十六-再见雍和宫
  20. UOS国产操作系统试用图解+网络配置

热门文章

  1. Python3爬取影片入库
  2. 算法与数据结构1800题 之 栈和队列
  3. NoClassDefFoundError 解决方法
  4. 二度云抢先成为首批中国工信部(.vip/.xyz/.club)域名注册管理机构
  5. groovy学习笔记 - 目录
  6. 再学 GDI+[94]: TGPImage(14) - 增减图像的红、绿、蓝三色的成分
  7. NSMutableString可变字符串
  8. IronPython系列:Observer Pattern及其实现
  9. 解决Eclipse java build path中Web App Libraries无法自动找到WEB-INF的lib目录
  10. centos7安装docker-ce新版