切片类型
切片的定义
Go语言的切片(slice)切片是一个拥有相同类型元素的可变长度的序列,类似于Java的list。我们知道,数组的大小是一个确定值,因此,在某些需要动态地扩展容器大小的地方,数组将变得不太适用。而切片在数组的基础上做了一层封装,以便支持需要动态扩容的场景。
Go语言中切片的内部结构包含底层数组的地址、切片的大小和切片的容量三个部分。
切片的声明
方式一
// 声明一个int类型的切片
var i [] int
方式二 从数组中获取切片(初始化操作)
package main
import "fmt"
/**由数组产生切片
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-01 23:18:21
@version 1.0
*/
func main() {
a := [...] int {1,2,3,4,5,6,7,8,9,0}
//注意,这里的切片截取是[)即左包含,而右不包含的
i := a[0:4]
for index := range i{
fmt.Print(i[index])
}
}
输出
1234
方式三 通过make构造切片
// capacity代表切片容量,这是一个可选参数
slice = make([]T, length, capacity)
注意:
- 切片在未初始化之前是一个空切片(nil)
- 要检查切片是否为空,请始终使用
len(s) == 0来判断,而不应该使用s == nil来判断。 - 切片之间不能直接比较。切片唯一合法的比较操作是和
nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil
举个例子:
package main
/**切片比较演示
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 09:27:38
@version 1.0
*/
func main() {
var a [] int
b := [] int{} //或者 b := make([]int, 0)
println(a==nil)
println(b==nil)
}
输出
true
false
切片表达式
上一小节当中的从数组获取切片,也是切片表达式的一种,下面介绍其他几种写法。
省略开始位置
//这个数组a下面的例子中继续沿用
a := [...] int {1,2,3,4,5,6,7,8,9,0}
i := a[:4]
省略结束位置
i := a[4:]
开始、结束都省略
i := a[:]
提示: 切片表达式也可以用来操作切片,即上面的数组a也可以是一个切片。
len() 和 cap() 函数
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
请看下方的demo
package main
/**len()和cap()函数演示
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-01 23:18:21
@version 1.0
*/
func main() {
a := [...] int {1,2,3,4,5,6,7,8,9,0}
i := a[2:6]
//获取切片长度并打印
println(len(i))
//获取切片容量并打印
println(cap(i))
}
输出
4
8
看到这里,大家或许会对切片容量和长度这两个值感到疑惑。上文咱讲切片的定义时曾说到,切片是对数组的封装,内部结构包含底层数组的地址、切片的大小和切片的容量三个部分。
切片的长度等于切片结束下标值减去切片开始下标值:
length = endIndex-startIndex
切片容量capacity等于底层数组长度减去开始下标值
capacity = len(array) - startIndex
所以上面的例子中的切片长度和容量分别是4和8。而底层数组的地址指向的就是切片开始下标对应值所在的内存地址。
/**切片地层数组地址演示
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 09:27:38
@version 1.0
*/
func main() {
a := [...] int {1,2,3,4,5,6,7,8,9,0}
i := a[2:6]
println(&i[0])
println(&a[2])
}
输出
这也再一次证明了切片是引用类型。
切片的直接赋值和copy()函数
直接赋值
package main
/**切片的赋值
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 09:27:38
@version 1.0
*/
func main() {
a := [...] int {1,2,3,4,5,6,7,8,9,0}
i := a[2:6]
//将i直接赋值给j,i、j将共用一个底层数组a
j := i
println(&i)
println(&j)
}
注意
- 对切片i的操作将影响到切片j
举个例子
package main
/**切片的赋值
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 09:27:38
@version 1.0
*/
func main() {
a := [...] int {1,2,3,4,5,6,7,8,9,0}
i := a[2:6]
//将i直接赋值给j,i、j将共用一个底层数组a
j := i
i[0] = 6
println(j[0])
println(a[2])
}
输出
6
6
这不难理解,因为i、j都是一个保持对数组a的引用,因此,改变i的值实际上改变的是数组a的值,所以j也会受到影响。
copy()函数
package main
/**copy()函数的使用
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 09:27:38
@version 1.0
*/
func main() {
a := [...] int {1,2,3,4,5,6,7,8,9,0}
i := a[2:6]
j := make([]int,len(i),cap(i))
copy(j,i)
i[0] = 6
println(j[0])
println(a[2])
}
输出
3
6
显然,这里的i、j所对应的底层数组不是同一个。
append()函数的使用
为切片添加新元素
Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(切片参数后面加…)
package main
import (
"fmt"
)
/**append()函数的使用
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 09:59:36
@version 1.0
*/
func main() {
//声明一个int类型的切片
var a [] int
b := [] int{5,6,7}
//向切片a中追加元素
a = append(a, 1,2,4)
//将切片a的元素追加切片b
b = append(b,a...)
fmt.Println(a)
fmt.Println(b)
}
输出
[1 2 4]
[5 6 7 1 2 4]
移除元素
在go语言当中,目前并没有为切片移除元素的函数,因此我们使用append()来做移除操作。
package main
import "fmt"
/**切片移除元素
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 10:23:51
@version 1.0
*/
func main() {
a := [] int{5,6,7}
//删除索引为1的元素
a = append(a[:1],a[2:]...)
fmt.Println(a)
}
输出
[5 7]
提示
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
切片的排序
对int类型的元素排序
package main
import (
"fmt"
"sort"
)
/**切片排序
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 10:23:51
@version 1.0
*/
func main() {
a := [] int{9,6,7}
//对int类型的元素进行排序
sort.Ints(a)
fmt.Println(a)
}
输出
[6 7 9]
对string类型的元素排序
package main
import (
"fmt"
"sort"
)
/**切片排序
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 10:23:51
@version 1.0
*/
func main() {
s := [] string {"zhangsan","wangwu","lisi","zhaoliu","suiqi","songjiu"}
sort.Strings(s)
fmt.Println(s)
}
输出
[lisi songjiu suiqi wangwu zhangsan zhaoliu]
查找元素位置
package main
import (
"fmt"
"sort"
)
/**切片查找元素位置
@author 赖柄沣 bingfengdev@aliyun.com
@date 2020-10-02 10:23:51
@version 1.0
*/
func main() {
s := [] string {"张三","王五","李四","赵六","孙七","宋九"}
sort.Strings(s)
fmt.Println(s)
a := sort.SearchStrings(s,"李四")
fmt.Println(s[a])
fmt.Println(a)
}
输出
[张三 王五 李四 赵六 孙七 宋九]
王五
1
注意
- 查找之前需要先对切片进行排序
切片的扩容策略
以下源码摘自/src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
//省略了不影响理解的部分代码
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
//省略了不影响理解的部分代码
}
分析:
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap);
- 否则,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap);
- 否则,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap);
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
注意:
切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
写在最后
在下一篇文章当中,将学习Go语言当中的map容器。