Go语言基础之切片

1 切片介绍

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

1.1 切片的内部实现

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

Go语言基础之切片

图1 切片在内存中的形式

2 切片的使用

2.1 基于数组定义切片

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

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

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

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

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

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

2.3 直接定义并赋值

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

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

方式一和方式二的区别:

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

2.4 切片再切片

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

func main() {
    // 切片再切片
    // 不仅可以基于数组和make函数来创建切片,也可以通过切片创建切片

    cityArray := [...]string{"北京", "上海", "广州", "深圳", "成都", "重庆"}
    fmt.Printf("cityArray:%v type:%T len:%d cap:%d\n", cityArray, cityArray, len(cityArray), cap(cityArray))
    citySlice := cityArray[1:3]
    fmt.Printf("citySlice:%v type:%T len:%d cap:%d\n", citySlice, citySlice, len(citySlice), cap(citySlice))
    newCitySlice := citySlice[1:5]
    fmt.Printf("newCitySlice:%v type:%T len:%d cap:%d\n", newCitySlice, newCitySlice, len(newCitySlice), cap(newCitySlice))
}

输出:

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  结构遍历切片。

func main() {
    s := []int{1, 3, 5}

    for i := 0; i < len(s); i++ {
        fmt.Println(i, s[i])
    }

    for index, value := range s {
        fmt.Println(index, value)
    }
}

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

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

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

输出:

[0]  len:1  cap:1  ptr:0xc0000a8000
[0 1]  len:2  cap:2  ptr:0xc0000a8040
[0 1 2]  len:3  cap:4  ptr:0xc0000b2020
[0 1 2 3]  len:4  cap:4  ptr:0xc0000b2020
[0 1 2 3 4]  len:5  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5]  len:6  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6]  len:7  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6 7]  len:8  cap:8  ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8]  len:9  cap:16  ptr:0xc0000b8000
[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()函数还支持一次性追加多个元素

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

4.2 删除切片中的元素  

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

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

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

 5 切片的扩容原则

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

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 类型的处理方式就不一样。

6 切片的拷贝操作

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

6.1 切片的赋值拷贝

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

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

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

func test(slice []int){
    slice[0] = 100
}

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

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

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

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

对上面代码的说明:

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

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

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

7 string和slice

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

Go语言基础之切片

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

8 练习题

  • 请写出下面代码的输出结果:
func main() {
    var a = make([]string, 5, 10)
    for i := 0; i < 10; i++ {
        a = append(a, fmt.Sprintf("%v", i))
    }
    fmt.Println(a)
}
  • 请使用内置的 sort 包对数组 var a = [...]int{3, 7, 8, 9, 1} 进行排序

相关推荐