Go 学习笔记(11)— 切片定义、切片初始化、数组和切片差异、字符串和切片转换、len()、cap()、空 nil 切片、append()、copy() 函数、删除切片元素

1. 切片定义

Go 语言切片是对数组的抽象。 Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

SliceHeader 是切片在 go 的底层结构。

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

所有切片的大小相同;

  • array: 是指向底层数组的指针;
  • len: 是切片的长度,即切片中当前元素的个数;
  • cap: 是底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值;

将一个 slice 变量分配给另一个变量只会复制三个机器字。所以 拷贝大切片跟小切片的代价应该是一样的。

我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。

Go 语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合,如果将数据集合比作切糕的话,切片就是你要的“那一块”,切的过程包含从哪里开始(切片的起始位置)及切多大(切片的大小),容量可以理解为装切片的口袋大小,如下图所示。

切片结构和内存分配.jpg

声明一个未指定大小的数组来定义切片:

var varName []type

切片不需要说明长度。或使用 make() 函数来创建切片:

var varName []type = make([]type, len)

// 也可以简写为

varName := make([]type, len)

也可以指定容量,其中 capacity 为可选参数。

make([]type, length, capacity)

其中 type 是指切片的元素类型, length 指的是为这个类型分配多少个元素, capacity 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题,在初始化 的时候尽量补全 cap。这里 length 是数组的长度并且也是切片的初始长度。

a := make([]int, 2)
b := make([]int, 2, 10)

fmt.Println(a, b)	// [0 0] [0 0]
fmt.Println(len(a), len(b))	// 2 2

其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。

注意:使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

Go 编译器会自动为每个新创建的切片,建立一个底层数组,默认底层数组的长度与切片初始元素个数相同。

2. 切片初始化

  1. 直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3, 其 cap=len=3
s := []int{1,2,3 }
  1. 切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。初始化切片 s,是数组 arr 的引用
s := arr[:]
  1. 将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex]

从数组生成切片,代码如下:

var a  = [3]int{1, 2, 3}
fmt.Println(a, a[1:2]) // [1 2 3]  [2]
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]

基于数组创建的切片,它的起始元素从 low 所标识的下标值开始,切片的长度(len)是 high - low,它的容量是 max - low。而且,由于切片 sl 的底层数组就是数组 arr,对切片 sl 中元素的修改将直接影响数组 arr 变量。比如,如果我们将切片的第一个元素加 10,那么数组 arr 的第四个元素将变为 14:

sl[0] += 10
fmt.Println("arr[3] =", arr[3]) // 14

在 Go 语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色。切片就是数组的“描述符”,也正是因为这一特性,切片才能在函数参数传递时避免较大性能开销。因为我们传递的并不是数组本身,而是数组的“描述符”,而这个描述符的大小是固定的(见上面的三元组结构),无论底层的数组有多大,切片打开的“窗口”长度有多长,它都是不变的。此外,我们在进行数组切片化的时候,通常省略 max,而 max 的默认值为数组的长度。

  1. 缺省 endIndex 时将表示一直到 arr 的最后一个元素
s := arr[startIndex:]
  1. 缺省 startIndex 时将表示从 arr 的第一个元素开始
s := arr[:endIndex]
  1. 通过切片 s 初始化切片 s1
s1 := s[startIndex:endIndex]
  1. 通过内置函数 make() 初始化切片 s,[]int 标识为其元素类型为 int 的切片,由 make 创建的切片各元素默认为该类型零值。
s :=make([]int, len, cap)

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;

  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;

  • 当缺省开始位置时,表示从连续区域开头到结束位置;

  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;

  • 两者同时缺省时,与切片本身等效;

a := []int{1, 2, 3}
fmt.Println(a[:])	// [1 2 3]

a 是一个拥有 3 个元素的切片,将 a 切片使用 a[:] 进行操作后,得到的切片与 a 切片一致。

  • 两者同时为 0 时,等效于空切片,一般用于切片复位。
a := []int{1, 2, 3}
fmt.Println(a[0:0]) 	// []

根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。

3. 数组和切片声明差异

数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。

如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片。只有不指定值的时候,才会创建切片,如下所示。

// 创建有3个元素的整型数组
array := [3]int{10, 20, 30}

// 创建长度和容量都是3的整型切片
slice := []int{10, 20, 30}

4. 字符串和切片转换

s := "hello"
a := []byte(s)	// 将字符串转换为 byte 类型切片
b := []rune(s)	// 将字符串转换为 rune 类型切片

5. len() 和 cap() 函数

数组的容量永远等于其长度,都是不可变的。

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

package main

import "fmt"

func main() {
   var numbers = make([]int,3,5)

   printSlice(numbers)
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果为:

len=3 cap=5 slice=[0 0 0]

示例:

func main() {
	s1 := make([]int, 5)                           
	// 用make函数初始化切片时,如果不指明其容量,那么它就会和长度一致
	fmt.Printf("The length of s1: %d\n", len(s1))   // 5
	fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5
	fmt.Printf("The value of s1: %d\n", s1)         // [0 0 0 0 0]
	s2 := make([]int, 5, 8)
	fmt.Printf("The length of s2: %d\n", len(s2))   // 5
	fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 8
	fmt.Printf("The value of s2: %d\n", s2)         // [0 0 0 0 0]
}

当我们通过切片表达式基于某个数组或切片生成新切片的时候,如下

func main() {

	s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
	s4 := s3[3:6]
	fmt.Printf("The length of s4: %d\n", len(s4))   // 3
	fmt.Printf("The capacity of s4: %d\n", cap(s4)) // 5
	fmt.Printf("The value of s4: %d\n", s4)         // [4 5 6]
}

切片的容量代表了它的底层数组的长度,但这仅限于使用 make 函数或者切片值字面量初始化切片的情况。

更通用的规则是:一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。由于 s4 是通过在 s3 上施加切片操作得来的,所以 s3 的底层数组就是 s4 的底层数组。又因为,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。所以,s4 的容量就是其底层数组的长度 8 减去上述切片表达式中的那个起始索引 3,即 5。

	s5 := s4[:cap(s4)]
	fmt.Printf("The length of s5: %d\n", len(s5))   // 5
	fmt.Printf("The capacity of s5: %d\n", cap(s5)) // 5
	fmt.Printf("The value of s5: %#v\n", s5)        // []int{4, 5, 6, 7, 8}

6. 空切片与 nil 切片

切片是动态结构,只能与 nil 判定相等,不能互相判定相等。一个切片在未初始化之前默认为 nil ,长度为 0,创建 nil 切片

// 创建nil整型切片
var varName []int

只要在声明时不做任何初始化,就会创建一个 nil 切片。

利用初始化,通过声明一个切片可以创建一个空切片

// 使用make创建空的整型切片
slice := make([]int, 0)

// 使用切片字面量创建空的整型切片
slice := []int{}

不管是使用 nil 切片还是空切片,对其调用内置函数 appendlencap的效果都是一样的。

实例如下:

package main

import "fmt"

func main() {
   var numbers []int

   printSlice(numbers)

   if(numbers == nil){
      fmt.Printf("切片是空的")
   }
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出结果为:

len=0 cap=0 slice=[]
切片是空的

空切片和 nil 切片对比

var s1 []int
var s2 = []int{}
  • s1 是声明,还没初始化,是 nil 值,lencap 是 0,arraynil, 底层没有分配内存空间。和nil 比较返回 true
  • s2 初始化了,不是 nil 值,是空切片, 底层分配了内存空间,有地址。和 nil 比较返回 false

更推荐第一种写法

package main

import "fmt"

func main() {
var sl1 []int
var sl2 = []int{}

fmt.Printf("%T, %v, %p\n", sl1, sl1, sl1) // []int, [], 0x0
fmt.Printf("%T, %v, %p\n", sl2, sl2, sl2) // []int, [], 某个地址值

fmt.Println(sl1 == nil) // true
fmt.Println(sl2 == nil) // false

fmt.Println(len(sl1), cap(sl1)) // 0, 0
fmt.Println(len(sl2), cap(sl2)) // 0, 0

// fmt.Println(sl1[0]) 下标越界 panic
// fmt.Println(sl2[0]) 下标越界 panic

sl1 = append(sl1, 1) // 可以 append 操作
sl2 = append(sl2, 1) // 可以 append 操作
}

7. 切片截取

可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:

package main

import "fmt"

func main() {
   /* 创建切片 */
   numbers := []int{0,1,2,3,4,5,6,7,8}   
   printSlice(numbers)

   /* 打印原始切片 */
   fmt.Println("numbers ==", numbers)

   /* 打印子切片从索引1(包含) 到索引4(不包含)*/
   fmt.Println("numbers[1:4] ==", numbers[1:4])

   /* 默认下限为 0*/
   fmt.Println("numbers[:3] ==", numbers[:3])

   /* 默认上限为 len(s)*/
   fmt.Println("numbers[4:] ==", numbers[4:])

   numbers1 := make([]int,0,5)
   printSlice(numbers1)

   /* 打印子切片从索引  0(包含) 到索引 2(不包含) */
   number2 := numbers[:2]
   printSlice(number2)

   /* 打印子切片从索引 2(包含) 到索引 5(不包含) */
   number3 := numbers[2:5]
   printSlice(number3)

}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

8. append() 函数

Go 语言的内建函数 append() 可以为切片动态添加元素,不过需要注意的是,在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。

切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……

一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。

在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下简称原容量)的 2 倍。但是,当原切片的长度(以下简称原长度)大于或等于 1024 时,Go 语言将会以原容量的 1.25 倍作为新容量的基准(以下新容量基准)。新容量基准会被调整(不断地与 1.25 相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的。

另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。注意,与前面那种情况一样,最终的新容量在很多时候都要比新容量基准更大一些。

package main

import "fmt"

func main() {
	var a []int
	a = append(a, 1)                 // 追加1个元素
	a = append(a, 1, 2, 3)           // 追加多个元素, 手写解包方式
	a = append(a, []int{1, 2, 3}...) // 追加一个切片, 切片需要解包
	fmt.Println(a)

	var numbers []int

	for i := 0; i < 5; i++ {
		numbers = append(numbers, i)
		fmt.Printf("len: %d  cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)
	}
}

输出结果:

[1 1 2 3 1 2 3]
len: 1  cap: 1 pointer: 0xc0000180d0
len: 2  cap: 2 pointer: 0xc0000180f0
len: 3  cap: 4 pointer: 0xc0000141a0
len: 4  cap: 4 pointer: 0xc0000141a0
len: 5  cap: 8 pointer: 0xc00001a180

从内存地址可以看出:

每次进行 append 之后,如果没有扩容则是在同一个切片上增加元素,如果已经扩容,则会返回一个新的切片,因为内存地址不一样

除了在切片的尾部追加,我们还可以在切片的开头添加元素:

var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片

在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素:

var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片

每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。

package main

import "fmt"

func main() {
	var a = []int{1, 2, 3}
	a = append([]int{0}, a...)          // 在开头添加1个元素
	a = append([]int{-3, -2, -1}, a...) // 在开头添加1个切片
	fmt.Println(a)                      // [-3 -2 -1 0 1 2 3]

	var b []int
	b = append(a[:0], append([]int{10}, b[0:]...)...)      // 在第0个位置插入10
	b = append(a[:0], append([]int{1, 2, 3}, b[0:]...)...) // 在第0个位置插入切片
	fmt.Println(b)                                         // [1 2 3 10]
}

问题 2:切片的底层数组什么时候会被替换?

确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。

请记住,在无需扩容时,append 函数返回的是指向原底层数组的新切片,而在需要扩容时,append 函数返回的是指向新底层数组的新切片。所以,严格来讲,“扩容”这个词用在这里虽然形象但并不合适。不过鉴于这种称呼已经用得很广泛了,我们也没必要另找新词了。

只要新长度不会超过切片的原容量,那么使用 append 函数对其追加元素的时候就不会引起扩容。

基于一个已有数组建立的切片,一旦追加的数据操作触碰到切片的容量上限(实质上也是数组容量的上界),切片就会和原数组解除“绑定”,后续对切片的任何修改都不会反映到原数组中了

9. copy() 函数

Go 语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

copy() 函数的使用格式如下:

copy(destSlice, srcSlice) int

其中 srcSlice 为数据来源切片, destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice ),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致, copy() 函数的返回值表示实际发生复制的元素个数。

package main

import "fmt"

func main() {
	a := []int{10, 20, 30, 4, 5}
	slice2 := []int{5, 4, 3}
	copy(slice2, a)     // 只会复制 a 的前3个元素到slice2中
	fmt.Println(slice2) // [10 20 30]

	slice2 = []int{5, 4, 3}
	b := []int{0, 0, 0, 0, 0}
	ret := copy(b, slice2)      // 只会复制slice2的3个元素到b 的前3个位置
	fmt.Println(b)              // [5 4 3 0 0]
	fmt.Println("ret is ", ret) // ret is  3
}

9.1 切片索引生成的切片与原来的切片是同一个地址

如果多个切片指向同一底层数组,那么对其中一个切片的改变影响其它的切片。

package main

import "fmt"

func main() {
	s1 := []int{1, 2, 3, 4, 5}
	s2 := s1[0:3]	// 只是基于同一个底层数组生成了一个新的切片(或者说窗口)
	// s2 := make([]int, 3)
	copy(s2, s1)
	s2 = append(s2, 40)
	s1[2] = 30

	fmt.Printf("The length of s1: %d\n", len(s1))   // 5
	fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5
	fmt.Printf("The value of s1: %d\n", s1)         // [1 2 30 40 5]

	fmt.Printf("The length of s2: %d\n", len(s2))   // 4
	fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 5
	fmt.Printf("The value of s2: %d\n", s2)         // [1 2 30 40]

	fmt.Printf("The address of s1: %p\n", s1) // 0xc00001a330
	fmt.Printf("The address of s2: %p\n", s2) // 0xc00001a330
}

9.2 使用make 会生成新的切片

package main

import "fmt"

func main() {
	s1 := []int{1, 2, 3, 4, 5}
	// s2 := s1[0:3]
	s2 := make([]int, 3)
	copy(s2, s1)
	s2 = append(s2, 40)
	s1[2] = 30

	fmt.Printf("The length of s1: %d\n", len(s1))   // 5
	fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5
	fmt.Printf("The value of s1: %d\n", s1)         // [1 2 30 4 5]

	fmt.Printf("The length of s2: %d\n", len(s2))   // 4
	fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 6
	fmt.Printf("The value of s2: %d\n", s2)         // [1 2 3 40]

	fmt.Printf("The address of s1: %p\n", s1) // 0xc00001a300
	fmt.Printf("The address of s2: %p\n", s2) // 0xc00001a330
}

10. 切片删除元素

Go 语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

10.1 从开头位置删除

删除开头的元素可以直接移动数据指针:

a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):

a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素

还可以用 copy() 函数来删除开头的元素:

a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

10.2 从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 appendcopy 原地完成:

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

10.3 从尾部删除

a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

注意:
连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值