Go语言切片详解

1. 切片底层实现

1.1 切片简介

  Go语言中的切片是围绕动态数组的概念构建的,能够按需自动增加和缩小。切片的动态增加是经过内置函数append来实现的,还能够经过对切片再次切片来缩小一个切片的大小。由于切片在内存中是连续的,因此切片还能得到索引、迭代以及垃圾回收优化的好处。数组

1.2 切片底层实现

  切片的底层实现包含3个字段:指向底层数组的指针、切片访问的元素的个数(长度)、切片容许增加到的元素的个数(容量),以下图所示。切片能够理解为对底层数组进行了抽象,并提供了相关的操做方法。
      数据结构

 

2. 切片的基础操做

2.1 建立和初始化

  能够经过make、切片字面量来建立和初始化切片,也能够利用现有数组或切片直接建立切片(Go语言中的引用类型(slice、map、chan)不能使用new进行初始化)。闭包

  1. 使用make时,须要传入一个参数指定切片的长度,若是只指定长度,则切片的容量和长度相等。也能够传入两个参数分别指定长度和容量。不容许建立容量小于长度的切片。
// 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)
  1. 经过切片字面量来声明切片。
// 经过字面量声明切片,其长度和容量都为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}
  1. 基于现有数组或切片来建立切片的方法为:s := baseStr[low:high:max],low指定开始元素下标,high指定结束元素下标,max指定切片能增加到的元素下标。这三个参数均可以省略,low省略默认从下标0开始,high省略默认为最后一个元素下标,max省略默认是底层数组或切片的容量(这里也要注意max不能小于high)。这种方式下,切片的长度和容量的计算方式为:
len = hith - low
cap = max - low
s1 := baseStr[1:3:10]
fmt.Printf("len: %d, cap: %d\n", len(s1), cap(s1)) // len: 2, cap: 9

s2 := baseStr[1:3]
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[2:5]
ss2 := s1[3:8]
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

  基于同一个数组或切片建立的不一样切片都共享同一个底层数组。若是一个切片修改了该底层数组的共享部分,其余切片和原始数组或切片都能感知到。其底层数据结构以下面两个图所示:
  共享同一底层数组:
      app

  改变互相感知:
      函数

baseSlice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s1 := baseSlice[1:5:10]
s2 := baseSlice[2:7]

// 这里 baseSlice、s一、s2 都共享同一个底层数组
/*    s1 起始指针指向 baseSlice 下标为1的元素,能够访问到baseSlice下标为4的元素,
   能够经过append增长容量到baseSlice最后一个元素。
	  s1 起始指针指向 baseSlice 下标为1的元素,能够访问到baseSlice下标为4的元素,
   能够经过append增长容量到baseSlice最后一个元素。
*/

// 下面的例子能够看到,无论修改 baseSlice、s一、s2 中的哪一个,这几个切片能访问到的数据都会跟着改变

// 修改 baseSlice 下标为3元素
/*
	baseSlice: [1 2 3 999 5 6 7 8 9 10]
	s1: [2 3 999 5]
	s2: [3 999 5 6 7]
*/
baseSlice[3] = 999
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)

// 修改 s1 下标为1元素
/*
	baseSlice: [1 2 888 999 5 6 7 8 9 10]
	s1: [2 888 999 5]
	s2: [888 999 5 6 7]
*/
s1[1] = 888
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)

// 修改 s2 下标为2元素
/*
	baseSlice: [1 2 888 999 222 6 7 8 9 10]
	s1: [2 888 999 222]
	s2: [888 999 222 6 7]
*/
s2[2] = 222
fmt.Printf("baseSlice: %v\n", baseSlice)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)

2.2 nil和空切片

  用var s []int声明的切片若是未经初始化,就是nil切片。空切片是用make或字面量建立的切片,s := make([]int, 0)或者s := []int{}。空切片在底层数组包含0个元素,也没有分配任何存储空间。无论是空切片仍是nil切片,对其调用函数append、len和cap的效果都是同样的。nil切片和空切片底层结构以下:
      学习

      

2.3 切片增加

  切片的增加是经过调用append函数完成的。函数append老是会增长新切片的长度,而容量可能会改变,也可能不会改变,这取决于被操做切片的可用用量(注意:append不会修改传入的切片,而是会返回一个新的切片)。优化

// 建立一个整型切片
// 其长度和容量都是5个元素
slice := []int{10, 20, 30, 40, 50}

// 建立一个新切片
// 其长度为2 个元素,容量为4个元素
newSlice := slice[1:3]

// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice = append(newSlice, 60)

fmt.Printf("slice: %v\n", slice)       // slice: [10 20 30 60 50]
fmt.Printf("newSlice: %v\n", newSlice) // newSlice: [20 30 60]

  以上代码运行的底层结构以下图:
      3d

  由于newSlice在底层数组里还有额外的容量可用,append操做将可用的元素合并到切片的长度,并对其进行赋值。因为和原始的slice共享同一个底层数组,因此slice中索引为3的元素的值也被改动 。若是切片的底层数组没有足够的容量可用,append函数会建立一个新的底层数组,将被引用的现有的值复制到新的数组里,再追加新的值。指针

// 建立一个整型切片
// 其长度和容量都是4个元素
slice := []int{10, 20, 30, 40}

// 向切片追加一个新元素
// 将新元素赋值为50
newSlice := append(slice, 50)

// 改变newSlice中的某个值,发现原始slice的值并无变化
newSlice[2] = 999

fmt.Printf("slice: %v\n", slice)       // slice: [10 20 30 40]
fmt.Printf("newSlice: %v\n", newSlice) // newSlice: [10 20 999 40 50]

  当这个append操做完成后,newSlice拥有一个全新的底层数组,这个数组的容量是原来的两倍。
      code

  函数append会智能地处理底层数组的容量增加。在切片的容量小于1000时,老是会成倍的增加容量。一旦元素个数超过1000,容量的增加因子会设为1.25,也就是每次增长25%的容量。

2.4 迭代切片

  切片能够用range迭代,可是要注意:若是只用一个值接收range,则获得的只是切片的下标,用两个值接收range,则获得的才是下标和对应的值。

slice := []int{10, 20, 30, 40}

// 若是只用一个值接收range,则获得的只是切片的下标
for i := range slice {
	fmt.Println(i)
}

// 若是用两个值接收range,则获得的是下标和对应的值
for i, v := range slice {
	fmt.Println(i, v)
}

  须要强调的是,range建立了每一个元素的副本,而不是直接返回对该元素的引用。若是使用该值变量的地址做为指向每一个元素的指针,就会形成错误。

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[i])
}

2.5 在函数间传递切片

  Go语言中参数的传递都是以值的方式传递的,引用类型也不例外。由于类型自己包装的是一个指针,因此传递引用类型是把指针复制一份,而不会复制其底层数据结构。
      

 

3. 多维切片

  和多维数组相似。
 

4. 参考文献

  《Go语言实战》   《Go语言学习笔记》

相关文章
相关标签/搜索