append append-Golang 入门 : 切片(slice)
当使用字面量来声明切片时,其语法与使用字面量声明数组非常相似。二者的区别是:如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片。只有在 [] 中不指定值的时候,创建的才是切片。看下面的例子:
// 创建有 3 个元素的整型数组 myArray := [3]int{10, 20, 30} // 创建度和容量都是 3 的整型切片 mySlice := []int{10, 20, 30}
nil 和空切片
有时,程序可能需要声明一个值为 nil 的切片(也称nil切片)。只要在声明时不做任何初始化,就会创建一个 nil 切片
// 创建 nil 整型切片 var myNum []int
在 Golang 中,nil 切片是很常见的创建切片的方法。nil 切片可以用于很多标准库和内置函数。在需要描述一个不存在的切片时,nil 切片会很好用。比如,函数要求返回一个切片但是发生异常的时候。下图描述了 nil 切片的状态:
空切片和 nil 切片稍有不同,下面的代码分别通过 make() 函数和字面量的方式创建空切片:
// 使用 make 创建空的整型切片 myNum := make([]int, 0) // 使用切片字面量创建空的整型切片 myNum := []int{}
空切片的底层数组中包含 0 个元素,也没有分配任何存储空间。想表示空集合时空切片很有用,比如,数据库查询返回 0 个查询结果时。下图描述了空切片的状态:
不管是使用 nil 切片还是空切片,对其调用内置函数 append()、len() 和 cap() 的效果都是一样的。
为切片中的元素赋值
对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一样。使用 [] 操作符就可以改变某个元素的值,下面是使用切片字面量来声明切片:
// 创建一个整型切片 // 其容量和度都是 5 个元素 myNum := []int{10, 20, 30, 40, 50} // 改变索引为 1 的元素的值 myNum [1] = 25
通过切片创建新的切片
切片之所以被称为切片,是因为创建一个新的切片,也就是把底层数组切出一部分。通过切片创建新切片的语法如下:
slice[i:j] slice[i:j:k]
其中 i 表示从 slice 的第几个元素开始切,j 控制切片的度(j-i),k 控制切片的容量(k-i),如果没有给定 k,则表示切到底层数组的最尾部。下面是几种常见的简写形式:
slice[i:] // 从 i 切到最尾部 slice[:j] // 从最开头切到 j(不包含 j) slice[:] // 从头切到尾,等价于复制整个 slice
让我们通过下面的例子来理解通过切片创建新的切片的本质:
// 创建一个整型切片 // 其度和容量都是 5 个元素 myNum := []int{10, 20, 30, 40, 50} // 创建一个新切片 // 其度为 2 个元素,容量为 4 个元素 newNum := slice[1:3]
执行上面的代码后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分:
注意:截取新切片时的原则是 “左含右不含”。所以 newNum 是从 myNum 的 index=1 处开始截取,截取到 index=3 的前一个元素,也就是不包含 index=3 这个元素。所以,新的 newNum 是由 myNum 中的第2个元素、第3个元素组成的新的切片构,度为 2,容量为 4。切片 myNum 能够看到底层数组全部 5 个元素的容量,而 newNum 能看到的底层数组的容量只有 4 个元素。newNum 无法访问到底层数组的第一个元素。所以,对 newNum 来说,那个元素就是不存在的。
共享底层数组的切片
需要注意的是:现在两个切片 myNum 和 newNum 共享同一个底层数组。如果一个切片修改了该底层数组的共享
部分,另一个切片也能感知到(请参考前图):
// 修改 newNum 索引为 1 的元素 // 同时也修改了原切片 myNum 的索引为 2 的元素 newNum[1] = 35
把 35 赋值给 newNum 索引为 1 的元素的同时也是在修改 myNum 索引为 2 的元素:
切片只能访问到其度内的元素
切片只能访问到其度内的元素,试图访问超出其度的元素将会导致语言运行时异常。在使用这部分元素前,必须将其合并到切片的度里。下面的代码试图为 newNum 中的元素赋值:
// 修改 newNum 索引为 3 的元素 // 这个元素对于 newNum 来说并不存在 newNum[3] = 45
上面的代码可以通过编译,但是会产生运行时错误:
panic: runtime error: index out of range
切片扩容
相对于数组而言,使用切片的一个好处是:可以按需增加切片的容量。Golang 内置的 append() 函数会处理增加度时的所有操作细节。要使用 append() 函数,需要一个被操作的切片和一个要追加的值,当 append() 函数返回时,会返回一个包含修改结果的新切片。函数 append() 总是会增加新切片的度,而容量有可能会改变append,也可能不会改变,这取决于被操作的切片的可用容量。
myNum := []int{10, 20, 30, 40, 50} // 创建新的切片,其度为 2 个元素,容量为 4 个元素 newNum := myNum[1:3] // 使用原有的容量来分配一个新元素 // 将新元素赋值为 60 newNum = append(newNum, 60)
执行上面的代码后的底层数据结构如下图所示:
此时因为 newNum 在底层数组里还有额外的容量可用,append() 函数将可用的元素合并入切片的度,并对其进行赋值。由于和原始的切片共享同一个底层数组,myNum 中索引为 3 的元素的值也被改动了。
如果切片的底层数组没有足够的可用容量,append() 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值,此时 append 操作同时增加切片的度和容量:
// 创建一个度和容量都是 4 的整型切片 myNum := []int{10, 20, 30, 40} // 向切片追加一个新元素 // 将新元素赋值为 50 newNum := append(myNum, 50)
当这个 append 操作完成后,newSlice 拥有一个全新的底层数组,这个数组的容量是原来的两倍:
函数 append() 会智能地处理底层数组的容量增。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增因子会设为 1.25,也就是会每次增加 25%的容量(随着语言的演化,这种增算法可能会有所改变)。
限制切片的容量
在创建切片时,使用第三个索引选项引可以用来控制新切片的容量。其目的并不是要增加容量,而是要限制容量。允许限制新切片的容量为底层数组提供了一定的保护,可以更好地控制追加操作。
// 创建度和容量都是 5 的字符串切片 fruit := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
下面尝试使用第三个索引项来完成切片操作:
// 将第三个元素切片,并限制容量 // 其度为 1 个元素,容量为 2 个元素 myFruit := fruit[2:3:4]
这个切片操作执行后,新切片里从底层数组引用了 1 个元素,容量是 2 个元素。具体来说,新切片引用了 Plum 元素,并将容量扩展到 Banana 元素:
如果设置的容量比可用的容量还大,就会得到一个运行时错误:
myFruit := fruit[2:3:6]
panic: runtime error: slice bounds out of range
内置函数 append() 在操作切片时会首先使用可用容量。一旦没有可用容量,就会分配一个新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况append,对切片进行修改,很可能会导致随机且奇怪的问题,这种问题一般都很难调查。如果在创建切片时设置切片的容量和度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。这样就可以安全地进行后续的修改操作了:
myFruit := fruit[2:3:3] // 向 myFruit 追加新字符串 myFruit = append(myFruit, "Kiwi")
这里,我们限制了 myFruit 的容量为 1。当我们第一次对 myFruit 调用 append() 函数的时候,会创建一个新的底层数组,这个数组包括 2 个元素,并将水果 Plum 复制进来,再追加新水果 Kiwi,并返回一个引用了这个底层数组的新切片。因为新的切片 myFruit 拥有了自己的底层数组,所以杜绝了可能发生的问题。我们可以继续向新切片里追加水果,而不用担心会不小心修改了其他切片里的水果。可以通过下图来理解此时内存中的数据结构:
将一个切片追加到另一个切片
内置函数 append() 也是一个可变参数的函数。这意味着可以在一次调用中传递多个值。如果使用 … 运算符,可以将一个切片的所有元素追加到另一个切片里:
// 创建两个切片,并分别用两个整数进行初始化 num1 := []int{1, 2} num2 := []int{3, 4} // 将两个切片追加在一起,并显示结果 fmt.Printf("%vn", append(num1, num2...))
输出的结果为:
[1 2 3 4]
在返回的新的切片中,切片 num2 里的所有值都追加到了切片 num1 中的元素后面。
遍历切片
切片是一个集合,可以迭代其中的元素。Golang 有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的元素
myNum := []int{10, 20, 30, 40, 50} // 迭代每一个元素,并显示其值 for index, value := range myNum { fmt.Printf("index: %d value: %dn", index, value) }
输出的结果为:
index: 0 value: 10 index: 1 value: 20 index: 2 value: 30 index: 3 value: 40 index: 4 value: 50
当迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。需要强调的是,range 创建了每个元素的副本,而不是直接返回对该元素的引用。要想获取每个元素的地址,可以使用切片变量和索引值:
myNum := []int{10, 20, 30, 40, 50} // 修改切片元素的值 // 使用空白标识符(下划线)来忽略原始值 for index, _ := range myNum { myNum[index] += 1 } for index, value := range myNum { fmt.Printf("index: %d value: %dn", index, value) }
输出的结果为:
index: 0 value: 11 index: 1 value: 21 index: 2 value: 31 index: 3 value: 41 index: 4 value: 51
关键字 range 总是会从切片头部开始遍历。如果想对遍历做更多的控制,可以使用传统的 for 循环配合 len() 函数实现:
myNum := []int{10, 20, 30, 40, 50} // 从第三个元素开始迭代每个元素 for index := 2; index切片间的拷贝操作
Golang 内置的 copy() 函数可以将一个切片中的元素拷贝到另一个切片中,其函数声明为:
func copy(dst, src []Type) int它表示把切片 src 中的元素拷贝到切片 dst 中,返回值为拷贝成功的元素个数。如果 src 比 dst ,就截断;如果 src 比 dst 短,则只拷贝 src 那部分:
num1 := []int{10, 20, 30} num2 := make([]int, 5) count := copy(num2, num1) fmt.Println(count) fmt.Println(num2)运行这段单面,输出的结果为:
3 [10 20 30 0 0]3 表示拷贝成功的元素个数。
把切片传递给函数
函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低。
让我们创建一个包含 100 万个整数的切片,并将这个切片以值的方式传递给函数 foo():
myNum := make([]int, 1e6) // 将 myNum 传递到函数 foo() slice = foo(myNum) // 函数 foo() 接收一个整型切片,并返回这个切片 func foo(slice []int) []int { ... return slice }在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,度和容量字段分别需要 8 字节。由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组:
在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本。
总结
切片是 Golang 中比较有特色的一种数据类型,既为我们操作集合类型的数据提供了便利的方式,又能够高效的在函数间进行传递,因此在代码中切片类型被使用的相当广泛。
参考:
限 时 特 惠: 本每日持续更新海量各大内部创业教程,一年会员只需98元,全资源免费下载
优惠码(不再需要): xnbaoku
大鱼项目网 » append append-Golang 入门 : 切片(slice)