【手把手教你写Go】08.复合类型-数组和切片

367 阅读8分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

目录:

7.3 数组

7.3.1 概述

数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数组的长度。

 

数组⻓度必须是常量,且是类型的组成部分。 [2]int 和 [3]int 是不同类型。

var n int = 10
var a [n]int  //err, non-constant array bound n
var b [10]int //ok

7.3.2 操作数组

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。

var a [10]int
for i := 0; i < 10; i++ {
    a[i] = i + 1
    fmt.Printf("a[%d] = %d\n", i, a[i])
}

//range具有两个返回值,第一个返回值是元素的数组下标,第二个返回值是元素的值
for i, v := range a {
    fmt.Println("a[", i, "]=", v)
}

内置函数 len(长度) 和 cap(容量) 都返回数组⻓度 (元素数量):

a := [10]int{}
fmt.Println(len(a), cap(a))//10 10

初始化:

a := [3]int{1, 2}           // 未初始化元素值为 0
b := [...]int{1, 2, 3}      // 通过初始化值确定数组长度
c := [5]int{2: 100, 4: 200} // 通过索引号初始化元素,未初始化元素值为 0
fmt.Println(a, b, c)        //[1 2 0] [1 2 3] [0 0 100 0 200]

//支持多维数组
d := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
e := [...][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}} //第二维不能写"..."
f := [4][2]int{1: {20, 21}, 3: {40, 41}}
g := [4][2]int{1: {0: 20}, 3: {1: 41}}
fmt.Println(d, e, f, g)

相同类型的数组之间可以使用 == 或 != 进行比较,但不可以使用 < 或 >,也可以相互赋值:\

a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2}
fmt.Println(a == b, b == c) //true false

var d [3]int
d = a
fmt.Println(d) //[1 2 3]

7.3.3 在函数间传递数组

根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数之间传递变量时,总是以值的方式传递的。如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。

func modify(array [5]int) {
    array[0] = 10 // 试图修改数组的第一个元素
    //In modify(), array values: [10 2 3 4 5]
    fmt.Println("In modify(), array values:", array)
}
 
func main() {
    array := [5]int{1, 2, 3, 4, 5} // 定义并初始化一个数组
    modify(array)                  // 传递给一个函数,并试图在函数体内修改这个数组内容
    //In main(), array values: [1 2 3 4 5]
    fmt.Println("In main(), array values:", array)
}
 
数组指针做函数参数:
func modify(array *[5]int) {
    (*array)[0] = 10
    //In modify(), array values: [10 2 3 4 5]
    fmt.Println("In modify(), array values:", *array)
}
 
func main() {
    array := [5]int{1, 2, 3, 4, 5} // 定义并初始化一个数组
    modify(&array)                 // 数组指针
    //In main(), array values: [10 2 3 4 5]
    fmt.Println("In main(), array values:", array)
}

7.3.4 数组指针

一个int需要8套别墅,8个int需要64套别墅,一样的道理,只需要告诉别人首地址,然后后面64套都是我的,别人怎么知道64?因为通过类型和长度可以推导出来。

package main //必须有个main包
import "fmt"
func modify(p *[5]int) {
    (*p)[0] = 666
    fmt.Println("modify *a = ", *p) //modify *a =  [666 2 3 4 5]
}
func main() {
    a := [5]int{1, 2, 3, 4, 5} //初始化
    modify(&a) //地址传递
    fmt.Println("main: a = ", a) //main: a =  [666 2 3 4 5]
}

7.4 slice

7.4.1 概述

数组的缺点:长度在定义之后无法再次修改;数组是值类型,每次传递都将产生一份副本。

显然这种数据结构无法完全满足开发者的真实需求。Go语言提供了数组切片(slice)来弥补数组的不足。

切片并不是数组或数组指针,它通过内部指针和相关属性引⽤数组⽚段,以实现变⻓⽅案。

slice并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层array,slice的声明也可以像array一样,只是不需要长度。

图片2.png

7.4.2 切片的创建和初始化

slice和数组的区别:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。

var s1 []int //声明切片和声明array一样,只是少了长度,此为空(nil)切片
s2 := []int{}

//make([]T, length, capacity) //capacity省略,则和length的值相同
var s3 []int = make([]int, 0)
s4 := make([]int, 0, 0)

s5 := []int{1, 2, 3} //创建切片并初始化

注意:make只能创建slice、map和channel,并且返回一个有初始值(非零)。

提示:如果可以提前预估数组大小,建议根据预估的大小,创建切片,避免切片动态调整数组大小带来的数据拷贝的性能消耗。

7.4.3 切片的操作

7.4.3.1 切片截取

操作含义
s[n]切片s中索引位置为n的项
s[:]从切片s的索引位置0到len(s)-1处所获得的切片
s[low:]从切片s的索引位置low到len(s)-1处所获得的切片
s[:high]从切片s的索引位置0到high处所获得的切片,len=high
s[low:high]从切片s的索引位置low到high处所获得的切片,len=high-low
s[low:high:max]从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low
len(s)切片s的长度,总是<=cap(s)
cap(s)切片s的容量,总是>=len(s)

 

示例说明:

    array := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

 

操作结果lencap说明
array [:6:8][0 1 2 3 4 5]68省略 low
array[5:][5 6 7 8 9]55省略 high、 max
array[:3][0 1 2]310省略 high、 max
array[:][0 1 2 3 4 5 6 7 8 9]1010全部省略

 

7.4.3.2 切片和底层数组关系

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := s[2:5]       //[2 3 4]
s1[2] = 100        //修改切片某个元素改变底层数组
fmt.Println(s1, s) //[2 3 100] [0 1 2 3 100 5 6 7 8 9]

s2 := s1[2:6] // 新切片依旧指向原底层数组 [100 5 6 7]
s2[3] = 200
fmt.Println(s2) //[100 5 6 200]

fmt.Println(s) //[0 1 2 3 100 5 6 200 8 9]

7.4.3.3 内建函数

1) append

append函数向 slice 尾部添加数据,返回新的 slice 对象:

var s1 []int //创建nil切换
//s1 := make([]int, 0)
s1 = append(s1, 1)       //追加1个元素
s1 = append(s1, 2, 3)    //追加2个元素
s1 = append(s1, 4, 5, 6) //追加3个元素
fmt.Println(s1)          //[1 2 3 4 5 6]

s2 := make([]int, 5)
s2 = append(s2, 6)
fmt.Println(s2) //[0 0 0 0 0 6]

s3 := []int{1, 2, 3}
s3 = append(s3, 4, 5)
fmt.Println(s3)//[1 2 3 4 5]

append函数会智能地底层数组的容量增长,一旦超过原底层数组容量,通常以2倍容量重新分配底层数组,并复制原来的数据:

func main() {
    s := make([]int, 0, 1)
    c := cap(s)
    for i := 0; i < 50; i++ {
        s = append(s, i)
        if n := cap(s); n > c {
            fmt.Printf("cap: %d -> %d\n", c, n)
            c = n
        }
    }
    /*
        cap: 1 -> 2
        cap: 2 -> 4
        cap: 4 -> 8
        cap: 8 -> 16
        cap: 16 -> 32
        cap: 32 -> 64
    */
}
  1. copy

函数 copy 在两个 slice 间复制数据,复制⻓度以 len 小的为准,两个 slice 可指向同⼀底层数组。

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := data[8:]  //{8, 9}
s2 := data[:5] //{0, 1, 2, 3, 4}
copy(s2, s1)    // dst:s2, src:s1

fmt.Println(s2)   //[8 9 2 3 4]
fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9]

7.4.4 切片做函数参数

func test(s []int) { //切片做函数参数
    s[0] = -1
    fmt.Println("test : ")
    for i, v := range s {
        fmt.Printf("s[%d]=%d, ", i, v)
        //s[0]=-1, s[1]=1, s[2]=2, s[3]=3, s[4]=4, s[5]=5, s[6]=6, s[7]=7, s[8]=8, s[9]=9,
    }
    fmt.Println("\n")
}
 
func main() {
    slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    test(slice)
 
    fmt.Println("main : ")
    for i, v := range slice {
        fmt.Printf("slice[%d]=%d, ", i, v)
        //slice[0]=-1, slice[1]=1, slice[2]=2, slice[3]=3, 
        //slice[4]=4, slice[5]=5, slice[6]=6, slice[7]=7, slice[8]=8, slice[9]=9,
    }
    fmt.Println("\n")
}

提示:切片只是引用数组,所以效率非常高,例如在函数传参的时候,使用切片传递数组参数,不会复制数组。

7.4.5 cap和len的区别

简单点说,len(sli)表示可见元素有几个(即直接打印元素看到的元素个数),而cap(sli)表示所有元素有几个,比如:

arr := []int{2, 3, 5, 7, 11, 13}
sli := arr[1:4]
fmt.Println(sli) //[3 5 7]
fmt.Println(len(sli))//3
fmt.Println(cap(sli))//5

7.4.6 切片和底层数组

同一个数组可以生成多个切片,当修改A切片时,底层数组会直接变,如果B切片刚好引用了同一个地方,则也会被改变。

package main //必须有个main包
import "fmt"

func main() {
    a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
 
    s1 := a[2:5] 
    s2 := s1[1:7]
 
    s1[1] = 666
    fmt.Println("s1 = ", s1) //s1 =  [2 666 4]
    fmt.Println("a = ", a) //a =  [0 1 2 666 4 5 6 7 8 9]
 
    s2[1] = 777
    fmt.Println("s2 = ", s2) //s2 =  [666 777 5 6 7 8]
    fmt.Println("a = ", a) //a =  [0 1 2 666 777 5 6 7 8 9]
 
    fmt.Println("s1 = ", s1)//s1 =  [2 666 777]
}

所以可以看出,切片其实是通过指针,取的底层数组的一部分数据。

提示:可以从数组中定义切片,也可以从切片中定义新的切片。