Go语言基础之切片

1 切片介绍

  Golang提供数组这种存储相同类型数据的数据结构,由于在现实生活中一件事物的个数不是固定,比如说一个班级的学生人数等,然而数组的长度是固定,因此在Golang中很少直接使用数组。和数组相对应的类型是切片slice,其代表变长的序列,序列中每个元素都是相同的类型。

1.1 切片的内部实现

  如图1所示,可以看出切片的底层还是数组。slice从底层来说,其实就是一个数据结构(struct结构体),切片有3个字段的数据结构,这三个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。

 

图1 切片在内存中的形式

 

 

2 切片的使用

2.1 基于数组定义切片

  定义一个切片,然后让切片去引用一个已经创建好的数组,如下案例:

1 func main() {
2     // 方式1:基于数组定义切片,定义一个切片,然后让切片去引用已经创建好的数组
3     var arr = [...]int{1, 2, 3, 4, 5}
4     var slice = arr[1:3] //左包含右不包含
5     fmt.Printf("slice的类型%T\n", slice)    //slice的类型[]int
6     fmt.Println("slice=", slice)            //slice= [2 3]
7     fmt.Println("slice len =", len(slice))    //slice len = 2
8     fmt.Println("slice cap =", cap(slice))    //slice cap = 4
9 }

2.2 使用make()函数创建切片

  基本语法: var 切片名 []type = make([]type, len, [cap]) 

  参数说明: type 就是数据类型; len 大小; cap 指定切片容量,可选,如果分配了 cap ,则要求 cap >= len 。

 1 func main() {
 2     // 方式2:使用make函数创建切片
 3     var slice []float64 = make([]float64, 5, 10)
 4     slice[1] = 10
 5     slice[2] = 20
 6     // 对于切片,必须make使用
 7     fmt.Println(slice)                        //[0 10 20 0 0]
 8     fmt.Println("slice len =", len(slice))    //slice len = 5
 9     fmt.Println("slice cap =", cap(slice))    //slice cap = 10
10 }
  • 通过make方式创建切片可以指定切片的大小和容量
  • 如果没有给切片的各个元素赋值,那么就会使用默认值
  • 通过make方式创建的切片对应的数组是由make底层维护,对外不可见,即只能通过slice去访问各个元素

2.3 直接定义并赋值

  定义一个切片,直接就指定具体数组,使用原理类似make的方式

1 func main() {
2     // 方式3:定义一个切片,直接就指定具体数组
3     var slice []string = []string{"tom", "jerry", "mary"}
4     fmt.Println(slice)                     //[tom jerry mary]
5     fmt.Println("slice len =", len(slice)) //slice len = 3
6     fmt.Println("slice cap =", cap(slice)) //slice cap = 3
7 }

  方式一和方式二的区别:

  • 方式1是直接引用数组,这个数组是事先存在的,程序员是可见的
  • 方式2是通过make来创建切片,make也会创建一个数组,是由切片在底层进行维护,程序员不可见

2.4 切片再切片

  除了基于数组和make函数得到切片,我们还可以通过切片来得到切片。 

 1 func main() {
 2     // 切片再切片
 3     // 不仅可以基于数组和make函数来创建切片,也可以通过切片创建切片
 4 
 5     cityArray := [...]string{"北京", "上海", "广州", "深圳", "成都", "重庆"}
 6     fmt.Printf("cityArray:%v type:%T len:%d cap:%d\n", cityArray, cityArray, len(cityArray), cap(cityArray))
 7     citySlice := cityArray[1:3]
 8     fmt.Printf("citySlice:%v type:%T len:%d cap:%d\n", citySlice, citySlice, len(citySlice), cap(citySlice))
 9     newCitySlice := citySlice[1:5]
10     fmt.Printf("newCitySlice:%v type:%T len:%d cap:%d\n", newCitySlice, newCitySlice, len(newCitySlice), cap(newCitySlice))
11 }

  输出:

1 cityArray:[北京 上海 广州 深圳 成都 重庆] type:[6]string len:6 cap:6
2 citySlice:[上海 广州] type:[]string len:2 cap:5
3 newCitySlice:[广州 深圳 成都 重庆] type:[]string len:4 cap:4

  切片初始化时,仍然不能越界。范围在[0-len(arr)]之间,但可以动态增长。

  •  var slice = arr[0:end]  可以简写  var slice = arr[:end] 
  •  var slice = arr[:end] 可以简写  var slice = arr[:end] 
  •  var slice = arr[0:len(arr)]  可以简写  var slice = arr[:] 

3 切片的遍历

  切片的遍历和数组一样,也有两种方式: for 循环常规方式遍历; for-range  结构遍历切片。

 1 func main() {
 2     s := []int{1, 3, 5}
 3 
 4     for i := 0; i < len(s); i++ {
 5         fmt.Println(i, s[i])
 6     }
 7 
 8     for index, value := range s {
 9         fmt.Println(index, value)
10     }
11 }

4 appned()方法为切片添加元素

  Go语言的内建函数 appen() 可以为切片动态添加元素,每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在 appen() 函数调用时,所以通常都需要用原变量接收 append() 函数的返回值。

1 func main() {
2     //append()添加元素和切片扩容
3     var numSlice []int
4     for i := 0; i < 10; i++ {
5         numSlice = append(numSlice, i)
6         fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
7     }
8 }

  输出:

 1 [0]  len:1  cap:1  ptr:0xc0000a8000
 2 [0 1]  len:2  cap:2  ptr:0xc0000a8040
 3 [0 1 2]  len:3  cap:4  ptr:0xc0000b2020
 4 [0 1 2 3]  len:4  cap:4  ptr:0xc0000b2020
 5 [0 1 2 3 4]  len:5  cap:8  ptr:0xc0000b6000
 6 [0 1 2 3 4 5]  len:6  cap:8  ptr:0xc0000b6000
 7 [0 1 2 3 4 5 6]  len:7  cap:8  ptr:0xc0000b6000
 8 [0 1 2 3 4 5 6 7]  len:8  cap:8  ptr:0xc0000b6000
 9 [0 1 2 3 4 5 6 7 8]  len:9  cap:16  ptr:0xc0000b8000
10 [0 1 2 3 4 5 6 7 8 9]  len:10  cap:16  ptr:0xc0000b8000

  从上面的结果可以看出:

  •  append() 函数将元素追加到切片的最后并返回该切片;
  • 切片 numSlice 的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍;
  • 切片在动态增加元素时,如果有一次增加的元素超过切片的容量,go底层会抛弃原先的数组,创建一个新的数组newArr,将slice原来包含的元素拷贝到新的数组newArr,slice重新引用到newArr;注意newArr是在底层来维护的,程序员不可见。

4.1 一次追加多个元素

  append()函数还支持一次性追加多个元素

1 var citySlice []string
2 // 追加一个元素
3 citySlice = append(citySlice, "北京")
4 // 追加多个元素
5 citySlice = append(citySlice, "上海", "广州", "深圳")
6 // 追加切片
7 a := []string{"成都", "重庆"}
8 citySlice = append(citySlice, a...)    //...代表的含义是将切片a展开,将其中的元素一一添加到citySlice中
9 fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]

4.2 删除切片中的元素  

  由于切片没有特定的方法删除切片中的元素,于是使用 append() 函数结合切片本身的特性实现切片中元素的删除。例如:

1 func main() {
2     // 从切片中删除元素
3     a := []int{30, 31, 32, 33, 34, 35, 36, 37}
4     // 要删除索引为2的元素
5     a = append(a[:2], a[3:]...)
6     fmt.Println(a) //[30 31 33 34 35 36 37]
7 }

  要从切片 slice 中删除索引为 index 的元素,操作方式是 slice = append(slice[:index], slice[index+1:]...) 

5 切片的扩容原则

  可以通过查看 $GOROOT/src/runtime/slice.go 源码,其中扩容相关代码如下:

 1 newcap := old.cap
 2 doublecap := newcap + newcap
 3 if cap > doublecap {
 4     newcap = cap
 5 } else {
 6     if old.len < 1024 {
 7         newcap = doublecap
 8     } else {
 9         // Check 0 < newcap to detect overflow
10         // and prevent an infinite loop.
11         for 0 < newcap && newcap < cap {
12             newcap += newcap / 4
13         }
14         // Set newcap to the requested cap when
15         // the newcap calculation overflowed.
16         if newcap <= 0 {
17             newcap = cap
18         }
19     }
20 }

  从上面的代码可以看出以下内容:

  • 首先判断,如果新申请容量(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 类型的处理方式就不一样。

6 切片的拷贝操作

  切片是引用类型,在赋值拷贝和使用 copy() 函数拷贝时要注意二者的区别。

6.1 切片的赋值拷贝

  下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

1 func main() {
2     s1 := make([]int, 3) //[0 0 0]
3     s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
4     s2[0] = 100
5     fmt.Println(s1) //[100 0 0]
6     fmt.Println(s2) //[100 0 0]
7 }

  切片在函数之间传参时,也遵守引用传递机制,如果在函数中修改切片中的内容,那么也会改变实参。

 1 func test(slice []int){
 2     slice[0] = 100
 3 }
 4 
 5 func main(){
 6     var slice = []int{1, 2, 3, 4, 5}
 7     fmt.Println("slice =", slice)  // slice = [1 2 3 4 5]
 8     test(slice)
 9     fmt.Println("slice =", slice)  // slice = [100 2 3 4 5]
10 }

6.2 使用copy()函数复制切片

  切片使用copy内置函数完成拷贝,举例说明:

1 func main() {
2     // 切片的拷贝操作
3     var slice1 []int = []int{1, 2, 3, 4, 5}
4     var slice2 = make([]int, 10)
5     copy(slice2, slice1)  //将slice1复制到slice2
6     fmt.Println("slice1=", slice1)  // slice1= [1 2 3 4 5]
7     fmt.Println("slice2=", slice2)  // slice2= [1 2 3 4 5 0 0 0 0 0]
8 }

  对上面代码的说明:

  • copy(para1, para2)参数的数据类型是切片;
  • 按照上面的代码,slice1和slice2的数据空间是独立的,相互不影响,也就是说 slice1[0]=999 , slice2[0] 仍然是1;

  注意:如果在拷贝时,目标切片的容量小于源切片的容量,此时目标的容量有多少就存多少源切片的内容:

1 func main() {
2     var a []int = []int{1, 2, 3, 4, 5}
3     var slice = make([]int, 1)
4     fmt.Println(slice)  // [0]
5     copy(slice, a)
6     fmt.Println(slice)  //[1]
7 }

7 string和slice

  • string底层是一个byte数组,因此string也可以进程切片处理,案例演示:
1 func main() {
2     str := "abc"
3     slice := str[1:3]
4     fmt.Printf("%T\n", slice)  // string
5     fmt.Println("slice =", slice)  // slice = bc
6     fmt.Println(len(slice))  // 2
7 }
  • string和切片在内存的形式,以上面代码为例画出内存示意图

 

 

 

  • string是不可变得,也就是说不能通过 str[0] = 'z' 方式来修改字符串
  • 如果需要修改字符串,可以先将 string  ->  []byte 或者 []byte  -> 修改 -> 重新转成 string ,代码如下:
 1 func main() {
 2     str1 := "hello world"
 3     arr1 := []byte(str1)
 4     arr1[0] = 'z'
 5     str1 = string(arr1)
 6     fmt.Println("str1=", str1)
 7     /*
 8         细节,我们转成[]byte后,可以处理英文和数字,但是不能处理中文,
 9         原因是 []byte 按字节来处理,而一个汉字是3个字节,因此就会出现乱码
10         解决方法是将string转成[]rune即可,因为[]rune是按字符处理,兼容汉字
11     */
12     str2 := "Hello Golang"
13     arr2 := []rune(str2)
14     arr2[0] = ''
15     str2 = string(arr2)
16     fmt.Println("str2=", str2)
17 }

8 切片注意事项

  切片之间不能比较 ,不能使用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较操作是和 nil 比较。一个 nil 值得切片并没有底层数组,一个 nil 值得切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是 nil ,例如下面的示例:

1 var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
2 s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
3 s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

  所以判断一个切片是否是空的,要用 len(s) == 0 来判断,不应该使用 s == nil 来判断。

9 练习题

  • 请写出下面代码的输出结果:
1 func main() {
2     var a = make([]string, 5, 10)
3     for i := 0; i < 10; i++ {
4         a = append(a, fmt.Sprintf("%v", i))
5     }
6     fmt.Println(a)
7 }
View Code
  • 请使用内置的 sort 包对数组 var a = [...]int{3, 7, 8, 9, 1} 进行排序
posted @ 2020-02-17 15:00  Dabric  阅读(460)  评论(0编辑  收藏  举报
TOP