一、GO语言中slice的定义

slice 是一种结构体类型,在源码中的定义为:

src/runtime/slice.go

type slice struct {
array unsafe.Pointer
len int
cap int
}

从定义中可以看到,slice是一种值类型,里面有3个元素。array是数组指针,它指向底层分配的数组;len是底层数组的元素个数;cap是底层数组的容量,超过容量会扩容。

二、初始化操作

slice有三种初始化操作,请看下面代码:

package main
import "fmt"
func main() {
//1、make
a := make([]int32, 0, 5)
//2、[]int32{}
b := []int32{1, 2, 3}
//3、new([]int32)
c := *new([]int32)
fmt.Println(a, b, c)
}

这几种初始化方式,在底层实现是不一样的。有一种了解底层实现好的方法,就是看反汇编的调用函数。运行下面命令即可看到代码某一行的反汇编:

go tool compile -S plan9Test.go | grep plan9Test.go:行号

1、make初始化

make函数初始化有三个参数,第一个是类型,第二个长度,第三个容量,容量要大于等于长度。slice的make初始化调用的是底层的runtime.makeslice函数。

func makeslice(et *_type, len, cap int) slice {
// NOTE: The len > maxElements check here is not strictly necessary,
// but it produces a 'len out of range' error instead of a 'cap out of range' error
// when someone does make([]T, bignumber). 'cap out of range' is true too,
// but since the cap is only being supplied implicitly, saying len is clearer.
// See issue 4085.
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}
}

主要就是调用mallocgc分配一块 个数cap*类型大小 的内存给底层数组,然后返回一个slice,slice的array指针指向分配的底层数组。

2、[]int32{} 初始化

这种初始化底层是调用 runtime.newobject 函数直接分配相应个数的底层数组。

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}

3、new([]int32) 初始化

这种初始化底层也是调用 runtime.newobject ,new是返回slice 的地址,所以要取地址里面内容才是真正的slice。

三、reSlice(切片操作)

所谓reSlice,是基于已有 slice 创建新的 slice 对象,以便在容量cap允许范围内调整属性。

data := []int32{0,1,2,3,4,5,6}
slice := data[1:4:5] // [low:high:max]

切片操作有三个参数,low、high、max,新生成的 slice 结构体三个参数,指针array指向原slice 底层数组元素下标为low的位置, len = high - low, cap = max - low。如下图所示:

切片操作主要要注意的就是在原slice 容量允许范围,超出容量范围会报panic

四、append 操作

slice 的 append 操作是向底层数组尾部添加数据,返回 新的slice对象

请看下面一段代码:

package main
import (
"fmt"
)
func main() {
a := make([]int32, 1, 2)
b := append(a, 1)
c := append(a, 1, 2)
fmt.Printf("a的地址:%p, 第一个元素地址:%p,容量:%v\n", &a, &a[0], cap(a))
//a的地址:0xc42000a060, 第一个元素地址:0xc42001a090,容量:2
fmt.Printf("b的地址:%p, 第一个元素地址:%p,容量:%v\n", &b, &b[0], cap(b))
//b的地址:0xc42000a080, 第一个元素地址:0xc42001a090,容量:2
fmt.Printf("c的地址:%p, 第一个元素地址:%p,容量:%v\n", &c, &c[0], cap(c))
//c的地址:0xc42000a0a0, 第一个元素地址:0xc42001a0a0,容量:4
}

从上面代码的打印结果中可以看出:a 是一个底层数组有一个元素,容量为2的slice;append 1个元素后,没有超出容量,产生了一个新的slice b,a 和 b 底层数组首元素相同地址,说明a,b共用底层数组;append 2个元素,超出了容量,产生一个新的slice c,c的底层数组地址变了,容量也翻倍了。

那么得出结论,append操作的运行过程:

1、如果添加数据后没有超过原始容量,新的slice对象 和原始slice共用底层数组,len 数据会变化,cap数据不变;

2、添加数据后超过了容量那就会扩容,重新分配一个新的底层数组,然后拷贝底层数组数据过去,那么append后产生的新slice对象和原始的slice就没有任何关系了。

扩容机制

看汇编代码可以知道,扩容调用的是底层函数 runtime.growslice

这个函数是这样定义的:

func growslice(et *_type, old slice, cap int) slice {}

这个函数传入三个参数:slice的原始类型,原始slice,期望的最小容量;返回一个新的slice,新slice 至少是拥有期望的最小容量,元素从原slice copy过来。

扩容规则主要是下面这段代码:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}

扩容规则就是两点:

  1. 如果期望的最小容量大于原始的两倍容量时,那么新的容量就是等于期望的最小容量;
  2. 不满足第一种情况,那么判断原slice的底层数组元素长度是不是小于1024。小于1024,新容量是原来的两倍;大于等于1024 ,新容量是原来的1.25倍。

上面是扩容的基本规则判断,实际上扩容还要考虑到内存对齐情况:

var lenmem, newlenmem, capmem uintptr
const 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) * ptrSize
newlenmem = uintptr(cap) * ptrSize
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
newcap = int(capmem / et.size)
}
内存对齐之后,扩容的倍数就会 >= 2 或则 1.25 了。

为什么要内存对齐?

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

五、函数调用中实参和形参的相互影响

1、slice值传递,在调用函数中直接操作底层数组

来看下面一段代码:

package main
import (
"fmt"
) func OpSlice(b []int32) {
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
//len: 5, cap: 5, data:[1 2 3 4 5]
fmt.Printf("b第一个元素地址:%p\n", &b[0])
//b第一个元素地址:0xc420016120 b[0] = 100
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
//len: 5, cap: 5, data:[100 2 3 4 5]
fmt.Printf("b第一个元素地址:%p\n", &b[0])
//b第一个元素地址:0xc420016120
} func main() {
a := []int32{1, 2, 3, 4, 5}
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
//len: 5, cap: 5, data:[1 2 3 4 5]
fmt.Printf("a第一个元素地址:%p\n", &a[0])
//a第一个元素地址:0xc420016120
OpSlice(a) fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
//len: 5, cap: 5, data:[100 2 3 4 5]
fmt.Printf("a第一个元素地址:%p\n", &a[0])
//a第一个元素地址:0xc420016120
}

从这段代码的打印中可以看到:

main函数中的slice a 是实参,值传递给调用函数时,要临时拷贝一份给b,所以a,b 的地址是不一样的,slice b 结构体中的三个元素都是a中的拷贝,但是元素array是指针,指针的拷贝还是指针,他们指向同一块底层数组,所以a,b底层数组的第一个元素地址是一样的。a,b共用同一块底层数组,在调用函数中,直接改变b的第一个元素内容,函数返回后a的第一个元素也变了,相当于改变了实参。

2、slice 指针传递

slice 指针传递就没什么说的了,在被调用函数中相当于操作的是实参中同一个slice,所有修改都会反映到实参。

3、slice 切片传递

不扩容的情况,来看下面一段代码:

package main
import (
"fmt"
)
func OpSlice(b []int32) {
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
//len: 3, cap: 9, data:[1 2 3]
fmt.Printf("b第一个元素地址:%p\n", &b[0])
//b第一个元素地址:0xc42007a064 b = append(b, 100)
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
//len: 4, cap: 9, data:[1 2 3 100]
fmt.Printf("b第一个元素地址:%p\n", &b[0])
//b第一个元素地址:0xc42007a064 } func main() {
a := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Printf("a第2个元素地址:%p\n", &a[1])
//a第2个元素地址:0xc42007a064 fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
//len: 10, cap: 10, data:[0 1 2 3 4 5 6 7 8 9]
fmt.Printf("a第一个元素地址:%p\n", &a[0])
//a第一个元素地址:0xc42007a060
OpSlice(a[1:4]) fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
//len: 10, cap: 10, data:[0 1 2 3 100 5 6 7 8 9]
fmt.Printf("a第一个元素地址:%p\n", &a[0])
//a第一个元素地址:0xc42007a060 }

前面已经讲过,切片和原slice是共用底层数组的。不扩容情况下,对切片产生的新的slice append 操作,新增加的元素会添加到底层数组尾部,会覆盖原有的值,反映到原slice中去;

总结

无论是slice的什么操作:拷贝,append,reSlice 等等都会产生新的slice,但是他们是共用底层数组的,不扩容情况,他们增删改元素都会影响到原来的slice底层数组;扩容情况下,产生的是一个“真正的”新的slice对象,和原来的完全独立开了,底层数组完全不会影响。

参考资料

  1. 深度解密Go语言之Slice.
  2. Go语言学习笔记.

GO语言slice详解(结合源码)的更多相关文章

  1. 基于双向BiLstm神经网络的中文分词详解及源码

    基于双向BiLstm神经网络的中文分词详解及源码 基于双向BiLstm神经网络的中文分词详解及源码 1 标注序列 2 训练网络 3 Viterbi算法求解最优路径 4 keras代码讲解 最后 源代码 ...

  2. 详解 QT 源码之 Qt 事件机制原理

    QT 源码之 Qt 事件机制原理是本文要介绍的内容,在用Qt写Gui程序的时候,在main函数里面最后依据都是app.exec();很多书上对这句的解释是,使 Qt 程序进入消息循环.下面我们就到ex ...

  3. Android应用AsyncTask处理机制详解及源码分析

    1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点.前面我们分析了Handler异步机制原理(不了解的可以阅读我的<Android异步消息处理机 ...

  4. SpringBoot之DispatcherServlet详解及源码解析

    在使用SpringBoot之后,我们表面上已经无法直接看到DispatcherServlet的使用了.本篇文章,带大家从最初DispatcherServlet的使用开始到SpringBoot源码中Di ...

  5. Java SPI机制实战详解及源码分析

    背景介绍 提起SPI机制,可能很多人不太熟悉,它是由JDK直接提供的,全称为:Service Provider Interface.而在平时的使用过程中也很少遇到,但如果你阅读一些框架的源码时,会发现 ...

  6. Spring Boot启动命令参数详解及源码分析

    使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring ...

  7. select用法&原理详解(源码剖析)(转)

    今天遇到了在select()前后fd_set的变化问题,查了好久终于找到一个有用的帖子了,很赞,很详细!!原文链接如下: select用法&原理详解(源码剖析) 我的问题是: 如下图示:在se ...

  8. 【转载】Android应用AsyncTask处理机制详解及源码分析

    [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个 ...

  9. 详解HashMap源码解析(下)

    上文详解HashMap源码解析(上)介绍了HashMap整体介绍了一下数据结构,主要属性字段,获取数组的索引下标,以及几个构造方法.本文重点讲解元素的添加.查找.扩容等主要方法. 添加元素 put(K ...

  10. 详解ConCurrentHashMap源码(jdk1.8)

    ConCurrentHashMap是一个支持高并发集合,常用的集合之一,在jdk1.8中ConCurrentHashMap的结构和操作和HashMap都很类似: 数据结构基于数组+链表/红黑树. ge ...

随机推荐

  1. 容器环境的JVM内存设置最佳实践

    Docker和K8S的兴起,很多服务已经运行在容器环境,对于java程序,JVM设置是一个重要的环节.这里总结下我们项目里的最佳实践. Java Heap基础知识 默认情况下,jvm自动分配的heap ...

  2. 关于程序员须知的 linux 基础

    我在 github 上新建了一个仓库 日问,每天一道面试题,有关前端,后端,devops以及软技能,促进职业成长,敲开大厂之门,欢迎交流 并且记录我的面试经验 17年面试记(阿里百度美团头条小米滴滴) ...

  3. 带 sin, cos 的线段树 - 牛客

    链接:https://www.nowcoder.com/acm/contest/160/D来源:牛客网 题目描述给出一个长度为n的整数序列a1,a2,...,an,进行m次操作,操作分为两类.操作1: ...

  4. 安全检测检查清单(Web)网站

    (一) 检查项:弱锁定机制 优先级:高 检查要点:检查系统帐号锁定机制健壮性 检查方法: 1.尝试使用错误的密码登录5次,查看账户是否被锁定2.等待10分钟再次测试,确认该用户是否还处于锁定状态 (二 ...

  5. 【转】document.form.action,表单分向提交

    document.form.action,表单分向提交,javascript提交表单 同一个表单可以根据用户的选择,提交给不同的后台处理程序.即,表单的分向提交.如,在编写论坛程序时,如果我们希望实现 ...

  6. 【Oracle】分区表详解

    此文从以下几个方面来整理关于分区表的概念及操作: 1.表空间及分区表的概念     2.表分区的具体作用     3.表分区的优缺点     4.表分区的几种类型及操作方法     5.对表分区的维护 ...

  7. java自定义注解学习(注解处理器)

    如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了.使用注解的过程中,很重要的一部分就是创建于使用注解处理器.Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处 ...

  8. POJ Protecting the Flowers

    点击打开题目 题目大意 奶牛要吃花,FJ来赶牛,将第i头牛赶走要2*ti分钟,奶牛每分钟吃di个单位花,求花的最小损失 先赶吃花多的,Wrong Answer QAQ 我们可以算一算损失 设sum=d ...

  9. c#数字图像处理(一)Bitmap类、 Bitmapdata类和 Graphics类

    Bitmap类. Bitmapdata类和 Graphics类是C#图像处理中最重要的3个类,如果要用C#进行图像处理,就一定要掌握它们. 1.1 Bitmap类Bitmap对象封装了GDI+中的一个 ...

  10. HTML超全笔记

    HTML概述 概念:是最基础的网页开发语言 Hyper Text Markup Language 超文本标记语言 超文本: 超文本是用超链接的方法,将各种不同空间的文字信息组织在一起的网状文本. 标记 ...