赞
踩
Go数组的长度是固定的,不能动态扩展,在有些场景中不太适用。
所以,Go提供了一种建立在数组之上的更强大的类型,叫做切片。
切片表示一个拥有相同类型元素的可变长度的序列。切片类型写作[]T,其中T是切片元素的类型。
切片和数组的关系非常密切,实际上,切片是一个轻量的数据结构,它内部引用了一个底层数组,同时还有另外零个属性:长度和容量,分别用来指明数组中实际被引用的元素和数组最大可用空间。
Go的内置函数len()和cap()用来返回切片的长度和容量。
// 声明一个元素类型为T的切片。
// 注意: 声明切片时[]中没有指定长度,而声明数组时需要指定长度。这是他们的区别。
var identifier []T
上面的语句声明了一个变量identifier,他的值被初始化为nil切片,nil切片的长度和容量都是0。
这里先记住nil切片的概念,在后续的内容中将再次对nil切片做介绍。
示例:
var slice1 []int
fmt.Prinln(slice1==nil) // 输出 true
fmt.Println(slice1) // 输出 []
fmt.Println(len(slice1)) // 切片长度,输出 0
fmt.Println(cap(slice1)) // 切片容量,输出 0
方式一(通过字面量创建切片):
[]T {value1, value2, value3, ...}
上面的语句根据{}中的元素个数来创建切片,切片的长度和容量等于{}中的元素个数。
特别的,如果{}中不含有任何元素,将会创建一个空切片,且长度和容量都是0。
这里先记住空切片的概念,在后续的内容中将再次空切片做介绍。
示例:
var s = []int{1, 2, 3}
fmt.Println(s) // [1 2 3]
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 3
// 空切片,不含任何元素的切片
s = []int{}
fmt.Println(s) // []
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
方式二(使用make()函数创建切片):
// 创建长度和容量都为len的切片,元素类型为T,元素初始化为T型的零值。
make([]T, len)
// 创建长度为len,容量为cap的切片,元素类型为T,元素初始化为T型的零值。
make([]T, len, cap)
上面的语句根据len和cap的值来创建切片,如果len不是0,切片中的元素将会初始化为T型的零值。
特别的,如果使用len=0来创建切片,得到的也是空切片。
示例:
var s = make([]int, 3) fmt.Println(s) // [0 0 0] 可以看到每个元素都被初始化为0零值 fmt.Println(len(s)) // 3 fmt.Println(cap(s)) // 3 s = make([]int, 3, 6) fmt.Println(s) // [0 0 0] fmt.Println(len(s)) // 3 fmt.Println(cap(s)) // 6 // 空切片 s = make([]int, 0) fmt.Println(s) // [] fmt.Println(len(s)) // 0 fmt.Println(cap(s)) // 0 // 也是空切片 s = make([]int, 0, 6) fmt.Println(s) // [] fmt.Println(len(s)) // 0 fmt.Println(cap(s)) // 6
长度:表示切片当前实际可被访问的元素个数。
容量:>=长度。是为向切片追加元素所预留的空间。如果向切片追加元素时容量>长度,那么直接使用原有的底层数组即可,只需要修改一下长度值。如果向切片追加元素时容量==长度,那么就需要重新分配一个更大的底层数组。
关于如何向切片中追加元素,在后续的内容中将会介绍。
可以像数组一样,通过下标来访问切片的元素。
下标从0开始的且必须小于切片的长度,否则会引发越界异常。
读取切片元素:
var s = []int{1, 2, 3, 4, 5, 6}
fmt.Println(s[0])
fmt.Println(s[1])
修改切片元素:
var s = []int{1, 2, 3, 4, 5, 6}
s[0] = 100
s[1] = 200
fmt.Println(s[0])
fmt.Println(s[1])
下标越界将会引发异常:
var s = []int{1, 2, 3, 4, 5, 6} // len = 6
s[6] = 100 // 引发越界异常
fmt.Println(s[6]) // 引发越界异常
我们可以使用for循环来遍历切片:
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for i:=0; i < len(s1); i++ {
fmt.Println("index:", i, ", value:", s1[i])
}
还可以使用for-range循环来遍历切片:
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for i, v := range s1 {
fmt.Println("index:", i, ", value:", v)
}
注意:range返回的是切片元素的复制,不是对切片元素的引用,所以我们在for-range循环中修改v值并不会改变切片里的值:
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for i, v := range s1 {
v = 100+i
_ = v
}
fmt.Println(s1) // 输出 [1 2 3 4 5 6 7 8 9 10]
如果需要修改切片元素的值,可使用普通的for循环来实现。
切片只支持和nil比较是否相等/不等,切片之间不可以比较。
s1 := []int{1, 2, 3, 4, 5}
s2 := []int{1, 2, 3, 4, 5}
fmt.Println(s1 == nil)
fmt.Println(s2 != nil)
// fmt.Println(s1 == s2) // 编译错误:slice can only be compared to nil
Go语言的切片是引用类型,也就是说在切片的数据结构中有一个指针指向了实际的数据。
切片的数据结构如下图:
由于切片是引用类型,所以当把切片复制给一个变量或传递给函数参数时,只是拷贝切片的数据结构部分,而不会拷贝指针指向的底层数组。
示例:
s1 := []int{}
var s2 []int
fmt.Println(s1 == nil) // false
fmt.Println(s2 == nil) // true
**注意:**在编程的时候,没有必要创建一个切片的指针来指向切片,因为切片本身已经是一个引用类型,它内部就包含指向实际数据的指针。
假设有一个切片型的变量slice,那么:
示例:
slice := []int{1, 2, 3, 4, 5}
// 输出切片底层数组的地址
fmt.Printf("slice: %p\n", slice) // slice: 0xc00000c300
// 输出切片数据结构的地址
fmt.Printf("&slice: %p\n", &slice) // &slice: 0xc0000044c0
// 而打印数组地址时,必须用&arr
arr := [...]int{1, 2, 3, 4, 5}
fmt.Printf("&arr: %p\n", &arr) // &arr: 0xc00000c390
fmt.Printf("arr: %p\n", arr) // arr: %!p([5]int=[1 2 3 4 5])
可以使用[m:n]
操作来截取切片,得到的新切片引用了原切片中从下标m到n(不含n)的元素。
m和n都可省略,如果省略m表示从下标0开始截取,如果省略n表示截取到最后一个元素。
m和n必须是原切片中的有效索引(即,<=原切片容量)。
示例:
numbers := []int{0,1,2,3,4,5,6,7,8}
fmt.Println(numbers) // [0 1 2 3 4 5 6 7 8]
n1 := numbers[1:4]
fmt.Println(n1) // [1 2 3]
n2 := numbers[:4]
fmt.Println(n2) // [0 1 2 3]
n3 := numbers[4:]
fmt.Println(n3) // [4 5 6 7 8]
n4 := numbers[:]
fmt.Println(n4) // [0 1 2 3 4 5 6 7 8]
当对切片执行截取操作后,得到的新切片与原切片共用一个底层数组。其数据结构图如下:
因此,当修改新切片的元素值时,原切片中对应位置的元素值也被改变。
numbers := []int{0,1,2,3,4,5,6,7,8}
fmt.Println(numbers) // [0 1 2 3 4 5 6 7 8]
n1 := numbers[1:4]
fmt.Println(n1) // [1 2 3]
// 修改新切片元素值
n1[0] = 100
n1[1] = 200
fmt.Println(n1) // [100 200 3]
fmt.Println(numbers) // [0 100 200 3 4 5 6 7 8]
对数组执行截取操作,得到的结果是一个切片:
numbers := [...]int{0,1,2,3,4,5,6,7,8} fmt.Println(numbers) // [0 1 2 3 4 5 6 7 8] n1 := numbers[1:4] fmt.Println(n1) // [1 2 3] n2 := numbers[:4] fmt.Println(n2) // [0 1 2 3] n3 := numbers[4:] fmt.Println(n3) // [4 5 6 7 8] n4 := numbers[:] fmt.Println(n4) // [0 1 2 3 4 5 6 7 8] // 修改新切片元素值将会改变原数组中的元素值 n1[0] = 100 n1[1] = 200 fmt.Println(n1) // [100 200 3] fmt.Println(numbers) // [0 100 200 3 4 5 6 7 8] fmt.Println(n2) // [0 100 200 3] fmt.Println(n3) // [4 5 6 7 8] fmt.Println(n4) // [0 100 200 3 4 5 6 7 8]
切片截取生成的新切片的长度和容量是多少?可按这个公式来计算:
假设底层数组的容量是k,对其进行切片[m:n]得到的新切片的长度为n-m,容量为k-m。
验证上面的公式:
numbers := make([]int, 6, 9)
n1 := numbers[1:3]
fmt.Printf("len=%d, cap=%d\n", len(n1), cap(n1))
根据上面的公式,n1长度=3-1=2,n1容量=9-1=8。执行上面的代码打印出:
len=2, cap=8
确实和公式计算出的结果一样。
截取时能否指定新切片的容量?
可以。Go语言还提供了一种使用3个索引的截取操作,第3个用来限定新切片的容量,语法格式为[m:n:k]。
通过这个操作截取得到的新切片长度=n-m,容量=k-m。当然,此处的k必须<=原切片容量,否则引发越界异常。
示例:
numbers := make([]int, 6, 9)
n1 := numbers[1:3:5]
fmt.Printf("len=%d, cap=%d\n", len(n1), cap(n1)) // len=2, cap=4
n2 := numbers[1:3:9]
fmt.Printf("len=%d, cap=%d\n", len(n2), cap(n2)) // len=2, cap=8
n3 := n1[0:2:4]
fmt.Printf("len=%d, cap=%d\n", len(n3), cap(n3)) // len=2, cap=4
n4 := n1[0:2:5] // 越界异常:5 超过n1的容量了
fmt.Printf("len=%d, cap=%d\n", len(n4), cap(n4))
截取的时候通常比原切片的长度小。但如果截取时指定的索引超出原切片的长度,得到的新切片将引用原切片中的未被使用的部分。
通过这种方法我们可以改变切片的长度,但长度不能超过容量,做法如下:
// 设slice是切片,new_len值不大于slice的容量,但大于其长度
slice = slice[0:new_len] // 执行后slice将具有新的长度,新增加的元素的值不是零值,而是底层数组中该元素的原有值。
切片可以通过这种方式改变长度,直到占满了整个切片的容量。
为了从slice的中间移除一个元素,并保留剩余元素的顺序,可以使用函数copy来将高位索引的元素向前移动来覆盖被移除元素所在位置:
// i必须不能越界, 即 0 <= i < len(slice)
func remove(slice[]int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
func main() {
s := []int{1, 2, 3, 4, 5}
s = remove(s, 2)
fmt.Println(s) // 输出 [1 2 3 5]
}
切片是引用类型,向函数传递切片类型时,仅仅会拷贝切片的数据结构部分,而不会拷贝底层数组,传入函数的切片和原切片共用同一个底层数组。因此在函数内修改切片元素值,会改变原切片中的元素值。
示例:
package main import "fmt" func main() { slice := []int{1, 2, 3, 4, 5} fmt.Printf("%p\n", slice) myFunction(slice) fmt.Println(slice) } func myFunction(slice []int) { // 注意这里[]中没有长度。没有长度表示切片,有长度就是数组了。 fmt.Printf("%p\n", slice) slice[1] = 10 } // 执行后输出 // 0xc00000c300 // 0xc00000c300 // [1 10 3 4 5]
其实无论是值类型还是引用类型,在函数间传递的时候都是值传递。
只不过值类型变量的数据结构中直接存放数据,在传递的时候拷贝该数据结构,也就把整个值拷贝了。
引用类型变量的数据结构中存放的是指向数据的指针,在传递的时候只拷贝该数据结构,并不拷贝指针指向的数据。
同样的,如果向myFunction函数传入一个由数组截取而来的切片,在函数内修改切片元素值,原数组中的元素值也会改变:
package main import "fmt" func main() { arr := [...]int{1, 2, 3, 4, 5} fmt.Printf("%p\n", &arr) myFunction(arr[:]) fmt.Println(arr) } func myFunction(slice []int) { // 注意这里[]中没有长度。没有长度表示切片,有长度就是数组了。 fmt.Printf("%p\n", slice) slice[1] = 10 } // 执行后输出 // 0xc00000c300 // 0xc00000c300 // [1 10 3 4 5]
虽然不能访问长度之外的元素,但可以向切片追加元素来扩展其长度。在扩展长度的时候,如果容量不够,也会自动扩展容量。append()函数用来实现该功能。
append()函数的作用:向切片追加元素,原切片不变,返回一个具有新长度的新切片。
append()的执行过程:
首先检查原切片是否有足够容量来存放追加的元素。
=> 如果原切片容量足够容纳追加的元素,将会定义一个与原切片共用底层数组的具有新长度的新切片,然后将待追加的元素复制到底层数组中,并返回这个新切片。
=> 如果原切片容量不够容纳追加的元素,将会创建一个拥有全新的底层数组的新切片,新底层数组的容量足够容纳原切片的元素和待追加的元素,然后将原切片中的元素复制到新底层数组,再将待追加的元素复制到新底层数组后面。
以上过程可用伪代码描述:
func append(slice []T, ele T) { var sliceNew []T lenNew := len(slice) + 1 if lenNew <= cap(slice) { // slice容量足够,只需扩展其长度 sliceNew = slice[:lenNew] } else { // slice容量不够,需要扩展容量,此处采用将容量扩展一倍的策略,Go语言实际的扩展策略有所不同。 capNew := lenNew if capNew < 2*len(slice) { capNew = 2 * len(slice) } sliceNew = make([]T, lenNew, capNew) copy(sliceNew, slice) } sliceNew[len(slice)] = ele return sliceNew }
通常情况下,我们并不清楚一个append()调用会不会导致分配新的底层数组,所以我们不能假设原始的slice和调用append()之后返回的结果slice是否指向的是同一个底层数组。所以,我们也无法假设对旧slice上的元素操作会不会影响西悉尼的slice元素。所以,我们通常将append()调用结果再次赋值给原slice:
slice := []int{1, 2, 3}
slice = append(slice, 4)
记住:不仅仅是在调用append函数的情况下需要更新slice变量,对于任何函数,只要有可能改变slice的长度或者容量,或者是使得slice指向不同的底层数组,都需要更新slice变量。
使用示例:
package main import "fmt" func main() { // 向空切片中append s1 := []int{} fmt.Printf("len = %d, cap = %d\n", len(s1), cap(s1)) // len = 0, cap = 0 fmt.Println("s1 = ", s1) // s1 = [] s1 = append(s1, 1) s1 = append(s1, 2) s1 = append(s1, 3) fmt.Printf("len = %d, cap = %d\n", len(s1), cap(s1)) // len = 3, cap = 4 fmt.Println("s1 = ", s1) // s1 = [1 2 3] // 向非空切片中append s2 := []int{1, 2, 3} fmt.Printf("len = %d, cap = %d\n", len(s2), cap(s2)) // len = 3, cap = 3 fmt.Println("s2 = ", s2) // s2 = [1 2 3] s2 = append(s2, 5) s2 = append(s2, 5) s2 = append(s2, 5) s2 = append(s2, 5) fmt.Printf("len = %d, cap = %d\n", len(s2), cap(s2)) // len = 7, cap = 12 fmt.Println("s2 = ", s2) // s2 = [1 2 3 5 5 5 5] // Go支持向nil切片中append var s3 []int fmt.Printf("len = %d, cap = %d\n", len(s3), cap(s3)) // len = 0, cap = 0 fmt.Println("s3 = ", s3) // s3 = [] s3 = append(s3, 1) fmt.Printf("len = %d, cap = %d\n", len(s3), cap(s3)) // len = 1, cap = 1 fmt.Println("s3 = ", s3) // s3 = [1] // 验证append返回的是一个新切片 s4 := make([]int, 3, 6) fmt.Printf("len = %d, cap = %d\n", len(s4), cap(s4)) // len = 3, cap = 6 fmt.Println("s4 = ", s4) // s4 = [0 0 0] s5 := append(s4, 1) fmt.Printf("len = %d, cap = %d\n", len(s4), cap(s4)) // len = 3, cap = 6 fmt.Println("s4 = ", s4) // s4 = [0 0 0] !!!原切片没有变 fmt.Printf("len = %d, cap = %d\n", len(s5), cap(s5)) // len = 4, cap = 6 fmt.Println("s5 = ", s5) // s5 = [0 0 0 1] }
对字符串做截取操作得到的结果是一个字符串,而不是切片类型。而且Go语言的字符串是不可变的。
str := "Hello World"
substr := str[0:5]
fmt.Printf("type:%T, value: %s\n", substr, substr) // 输出 type:string, value: Hello
//str[0] = 'h' // 编译错误:cannot assign to str[0]
//substr[0] = 'h' // 编译错误:cannot assign to substr[0]
可以从字符串生成[]byte或[]rune类型的切片:
str := "Hello 世界"
b := []byte(str)
r := []rune(str)
fmt.Println(b) // 输出 [72 101 108 108 111 32 228 184 150 231 149 140]
fmt.Println(r) // 输出 [72 101 108 108 111 32 19990 30028]
可以通过代码 len([]rune(s))
来获得字符串中字符的数量,但使用 utf8.RuneCountInString(s)
效率会更高一点。
如果想要修改字符串中的某些字符,那么可以先把字符串转为[]byte或[]rune类型切片,修改切片中的数据,最后再把切片转为字符串。
示例:
str := "Hello 世界"
b := []byte(str)
b[0] = 'h'
str2 := string(b)
fmt.Println(str2) // 输出 hello World
fmt.Println(str) // 输出 Hello World // 发现原字符串并没有变
r := []rune(str)
r[6] = '天'
str3 := string(r)
fmt.Println(str3) // 输出 Hello 天界
fmt.Println(str) // 输出 Hello 世界 // 原字符串并没有变
// push
stack = append(stack, v)
// empty
empty := len(stack) == 0
// top
top := stack[len(stack)-1]
// pop
stack = stack[:len(stack)-1]
Copyright@2022 , 359152155@qq.com
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。