Go语言切片详解
<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>1. 切片底层实现<ul><li>1.1 切片简介</li><li>1.2 切片底层实现</li></ul></li><li>2. 切片的基础操作<ul><li>2.1 创建和初始化</li><li>2.2 nil和空切片</li><li>2.3 切片增长</li><li>2.4 迭代切片</li><li>2.5 在函数间传递切片</li></ul></li><li>3. 多维切片</li><li>4. 参考文献</li></ul></div><p></p><h1 id="1-切片底层实现">1. 切片底层实现</h1>
<h2 id="11-切片简介">1.1 切片简介</h2>
<p> Go语言中的切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append来实现的,还可以通过对切片再次切片来缩小一个切片的大小。因为切片在内存中是连续的,所以切片还能获得索引、迭代以及垃圾回收优化的好处。</p>
<h2 id="12-切片底层实现">1.2 切片底层实现</h2>
<p> 切片的底层实现包含3个字段:指向底层数组的指针、切片访问的元素的个数(长度)、切片允许增长到的元素的个数(容量),如下图所示。切片可以理解为对底层数组进行了抽象,并提供了相关的操作方法。<br>
<img src="https://img2020.cnblogs.com/blog/2007834/202005/2007834-20200517221751082-44368043.jpg"></p>
<p> </p>
<h1 id="2-切片的基础操作">2. 切片的基础操作</h1>
<h2 id="21-创建和初始化">2.1 创建和初始化</h2>
<p> 可以通过make、切片字面量来创建和初始化切片,也可以利用现有数组或切片直接创建切片(<em><font color="red">Go语言中的引用类型(slice、map、chan)不能使用new进行初始化</font></em>)。</p>
<ol>
<li>使用make时,需要传入一个参数指定切片的长度,如果只指定长度,则切片的容量和长度相等。也可以传入两个参数分别指定长度和容量。不允许创建容量小于长度的切片。</li>
</ol>
<pre><code>// make只传入一个参数指定长度,则容量和长度相等。以下输出:"len: 5, cap: 5"
s := make([]int, 5)
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
// make 传入长度和容量。以下输出:"len: 5, cap: 10"
s := make([]int, 5, 10)
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
// 不允许创建容量小于长度的切片。下面语句编译会报错:"len larger than cap in make([]int)"
s := make([]int, 10, 5)
</code></pre>
<ol start="2">
<li>通过切片字面量来声明切片。</li>
</ol>
<pre><code>// 通过字面量声明切片,其长度和容量都为5。以下输出:“len: 5, cap: 5”
s := []int{1, 2, 3, 4, 5}
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
// 可以在声明切片时利用索引来给出所需的长度和容量。
// 通过指定索引为99的元素,来创建一个长度和容量为100的切片
s := []int{99: 0}
</code></pre>
<ol start="3">
<li>基于现有数组或切片来创建切片的方法为:<code>s := baseStr</code>,low指定开始元素下标,high指定结束元素下标,max指定切片能增长到的元素下标。这三个参数都可以省略,low省略默认从下标0开始,high省略默认为最后一个元素下标,max省略默认是底层数组或切片的容量(这里也要注意max不能小于high)。这种方式下,切片的长度和容量的计算方式为:</li>
</ol>
<pre><code>len = hith - low
cap = max - low
</code></pre>
<pre><code>s1 := baseStr
fmt.Printf("len: %d, cap: %d\n", len(s1), cap(s1)) // len: 2, cap: 9
s2 := baseStr
fmt.Printf("len: %d, cap: %d\n", len(s2), cap(s2)) // len: 2, cap: 9
s3 := baseStr[:3]
fmt.Printf("len: %d, cap: %d\n", len(s3), cap(s3)) // len: 3, cap: 10
ss1 := s1
ss2 := s1
fmt.Printf("len: %d, cap: %d\n", len(ss1), cap(ss1)) // len: 3, cap: 7
fmt.Printf("len: %d, cap: %d\n", len(ss2), cap(ss2)) // len: 5, cap: 6
</code></pre>
<p> 基于同一个数组或切片创建的不同切片都共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,其他切片和原始数组或切片都能感知到。其底层数据结构如下面两个图所示:<br>
共享同一底层数组:<br>
<img src="https://img2020.cnblogs.com/blog/2007834/202005/2007834-20200517221813707-1823090865.jpg"></p>
<p> 改变互相感知:<br>
<img src="https://img2020.cnblogs.com/blog/2007834/202005/2007834-20200517221824790-558462398.jpg"></p>
<pre><code>baseSlice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s1 := baseSlice
s2 := baseSlice
// 这里 baseSlice、s1、s2 都共享同一个底层数组
/* s1 起始指针指向 baseSlice 下标为1的元素,可以访问到baseSlice下标为4的元素,
可以通过append增加容量到baseSlice最后一个元素。
s1 起始指针指向 baseSlice 下标为1的元素,可以访问到baseSlice下标为4的元素,
可以通过append增加容量到baseSlice最后一个元素。
*/
// 下面的例子可以看到,不管修改 baseSlice、s1、s2 中的哪个,这几个切片能访问到的数据都会跟着改变
// 修改 baseSlice 下标为3元素
/*
baseSlice:
s1:
s2:
*/
baseSlice = 999
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)
// 修改 s1 下标为1元素
/*
baseSlice:
s1:
s2:
*/
s1 = 888
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)
// 修改 s2 下标为2元素
/*
baseSlice:
s1:
s2:
*/
s2 = 222
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)
</code></pre>
<h2 id="22-nil和空切片">2.2 nil和空切片</h2>
<p> 用<code>var s []int</code>声明的切片如果未经初始化,就是nil切片。空切片是用make或字面量创建的切片,<code>s := make([]int, 0)或者s := []int{}</code>。空切片在底层数组包含0个元素,也没有分配任何存储空间。不管是空切片还是nil切片,对其调用函数append、len和cap的效果都是一样的。nil切片和空切片底层结构如下:<br>
<img src="https://img2020.cnblogs.com/blog/2007834/202005/2007834-20200517221856525-819629741.jpg"></p>
<p> <img src="https://img2020.cnblogs.com/blog/2007834/202005/2007834-20200517221904770-1087248989.jpg"></p>
<h2 id="23-切片增长">2.3 切片增长</h2>
<p> 切片的增长是通过调用append函数完成的。函数append总是会增加新切片的长度,而容量可能会改变,也可能不会改变,这取决于被操作切片的可用用量(<strong><font color="red">注意:append不会修改传入的切片,而是会返回一个新的切片</font></strong>)。</p>
<pre><code>// 创建一个整型切片
// 其长度和容量都是5个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4个元素
newSlice := slice
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice = append(newSlice, 60)
fmt.Printf("slice: %v\n", slice) // slice:
fmt.Printf("newSlice: %v\n", newSlice) // newSlice:
</code></pre>
<p> 以上代码运行的底层结构如下图:<br>
<img src="https://img2020.cnblogs.com/blog/2007834/202005/2007834-20200517221920406-809855284.jpg"></p>
<p> 因为newSlice在底层数组里还有额外的容量可用,append操作将可用的元素合并到切片的长度,并对其进行赋值。由于和原始的slice共享同一个底层数组,所以slice中索引为3的元素的值也被改动 。如果切片的底层数组没有足够的容量可用,append函数会创建一个新的底层数组,将被引用的现有的值复制到新的数组里,再追加新的值。</p>
<pre><code>// 创建一个整型切片
// 其长度和容量都是4个元素
slice := []int{10, 20, 30, 40}
// 向切片追加一个新元素
// 将新元素赋值为50
newSlice := append(slice, 50)
// 改变newSlice中的某个值,发现原始slice的值并没有变化
newSlice = 999
fmt.Printf("slice: %v\n", slice) // slice:
fmt.Printf("newSlice: %v\n", newSlice) // newSlice:
</code></pre>
<p> 当这个append操作完成后,newSlice拥有一个全新的底层数组,这个数组的容量是原来的两倍。<br>
<img src="https://img2020.cnblogs.com/blog/2007834/202005/2007834-20200517221933205-1042819636.jpg"></p>
<p> 函数append会智能地处理底层数组的容量增长。在切片的容量小于1000时,总是会成倍的增长容量。一旦元素个数超过1000,容量的增长因子会设为1.25,也就是每次增加25%的容量。</p>
<h2 id="24-迭代切片">2.4 迭代切片</h2>
<p> 切片可以用range迭代,但是要注意:如果只用一个值接收range,则得到的只是切片的下标,用两个值接收range,则得到的才是下标和对应的值。</p>
<pre><code>slice := []int{10, 20, 30, 40}
// 如果只用一个值接收range,则得到的只是切片的下标
for i := range slice {
fmt.Println(i)
}
// 如果用两个值接收range,则得到的是下标和对应的值
for i, v := range slice {
fmt.Println(i, v)
}
</code></pre>
<p> 需要强调的是,range创建了每个元素的副本,而不是直接返回对该元素的引用。如果使用该值变量的地址作为指向每个元素的指针,就会造成错误。</p>
<pre><code>slice := []int{10, 20, 30, 40}
/*
下面的打印输出如下:
Value: 10, Value-Addr: C00000C168, ElemAddr: C000012560
Value: 20, Value-Addr: C00000C168, ElemAddr: C000012568
Value: 30, Value-Addr: C00000C168, ElemAddr: C000012570
Value: 40, Value-Addr: C00000C168, ElemAddr: C000012578
Value-Addr 表示的是遍历时用到的变量 v
ElemAddr 表示的是原来的切片slice里每个元素的地址
可以看出 range 在遍历时,将slice的每个元素都复制到了同一个变量 v 。
使用闭包的时候,尤其要注意range的这种特性。
*/
for i, v := range slice {
fmt.Printf("Value: %d, Value-Addr: %X, ElemAddr: %X\n",
v, &v, &slice)
}
</code></pre>
<h2 id="25-在函数间传递切片">2.5 在函数间传递切片</h2>
<p> <strong>Go语言中参数的传递都是以值的方式传递的,引用类型也不例外。因为类型本身包装的是一个指针,所以传递引用类型是把指针复制一份,而不会复制其底层数据结构。</strong><br>
<img src="https://img2020.cnblogs.com/blog/2007834/202005/2007834-20200517221943405-1339949018.jpg"></p>
<p> </p>
<h1 id="3-多维切片">3. 多维切片</h1>
<p> 和多维数组类似。<br>
</p>
<h1 id="4-参考文献">4. 参考文献</h1>
<p> 《Go语言实战》<br>
《Go语言学习笔记》</p><br><br>
来源:https://www.cnblogs.com/lvnux/p/12907356.html
頁:
[1]