探索Go语言:复数类型与切片

发表时间: 2023-09-25 16:28

slice

数组的长度在定义之后无法再次修改, 数组是值类型, 每次传递都将产生一个副本。虽然这种数据结构无法满足开发者的真实需求。

Go语言提供了数组切片(slice)来弥补数组的不足。

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

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

array := [...]int{10, 20, 30, 0, 0}

slice := array[0:3:5]

array [0] 10 [1] 20 [2] 30 [3] 0 [4] 0

slice 地址指针 长度3 容量5 整型切片, 长度为3个整型值, 容量为5个整型值

array:是指向下层数组某元素的指针, 该元素也是切片的起始元素;

len:是切片的长度, 即切片中当前元素的个数;

cap:是切片的最大容量, cap >= len。

[low:high:max]

low: 下标的起点

high: 下标的终点(不包含此下标), [a[low], a[high])左闭右开

max: 最大容量(可选), max值不能大于数组的长度, 也不能小于切片的长度(high - low), 默认值为数组的长度。

len = high - low, 长度

cap = max - low, 容量

提醒: len是切片长度, cap实际上是底层数组的长度, max则可以修改slice的容量

切片和数组的区别

package main //必须有个main包import(    "fmt")func main(){    //切片和数组的区别    //数组[]里面长度是固定的一个常量, 数组长度不能修改长度, len和cap永远都是5    a := [5]int{}    fmt.Printf("len = %d, cap = %d\n", len(a), cap(a))    //切片, []里面为空, 切片的长度或容量可以不固定    s := []int{}    fmt.Printf("len = %d, cap = %d\n", len(s), cap(s))    s = append(s, 10) //给切片末尾追加一个成员 append函数只能适用于切片    fmt.Printf("append : len = %d, cap = %d\n", len(s), cap(s))}

切片的创建和初始化

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

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

s2 := []int{} //简短声明方法, 后面的{}括号不能省略



//make([]T, length, capacity) //length参数不能省略, capacity省略, 则和length的值相同

var s3 []int = make([]int, 0) //创建

s3 = []int{1, 2, 3, 4, 5} //初始化

s4 := make([]int, 0, 0) //简短模式切片创建

s4 = []int{1, 2, 3, 4, 5} //初始化

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

package mainimport (    "fmt")func main() { //左括号必须和函数名同行    s1 := make([]int, 16)    s2 := make([]int, 10, 32)    fmt.Println(len(s1), cap(s1)) //16 16    fmt.Println(len(s2), cap(s2)) //10 32}

切片操作

切片截取

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)

注意:

low 和 high 是 slice 的索引(index), 其数值必须是整数, max参数用来指定返回的切片的容量

示例说明

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

操作 结果 len cap 说明

array[:6:8] [0 1 2 3 4 5] 6 8 省略low

array[5:] [5 6 7 8 9] 5 5 省略high、max

array[:3] [0 1 2] 3 10 省略high、max, 取长度, :3代表从开始位置取三个元素

array[:] [0 1 2 3 4 5 6 7 8 9] 10 10 全部省略

切片与底层数组关系

package main //必须有个main包import(    "fmt")func main(){    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}    s1 := s[2:5] //[2, 3, 4], 不包括 5, (5-2=3)取三个元素    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] 注意: 2:6  s1 切片的索引, 都是从底层拿到的数据, 对应底层 4:8  索引, 虽然 5 6 7 在切片 s2 不存在, 但仍然访问底层的数组    s2[3] = 200    fmt.Println(s2) //[100 5 6 200]    fmt.Println(s) //[0 1 2 3 100 5 6 200 8 9]}

指针直接访问底层数组, 退化成普通数组操作。

package mainimport "fmt"func main() {    s := []int{0, 1, 2, 3}    p := &s[2] // *int, 获取底层数组元素指针。    *p += 100    fmt.Println(s) // [0 1 102 3]}

内建函数

append

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

package main //必须有个main包import(    "fmt")func main(){    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) //[0, 0, 0, 0, 0]    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]}

使用 "..."追加

package mainimport (    "fmt")func main() {    var a = []int{1, 2, 3}    fmt.Printf("slice a : %v\n", a)    var b = []int{4, 5, 6}    fmt.Printf("slice b : %v\n", b)    c := append(a, b...) //使用 ... 追加    fmt.Printf("slice c : %v\n", c)    d := append(c, 7)    fmt.Printf("slice d : %v\n", d)    e := append(d, 8, 9, 10)    fmt.Printf("slice e : %v\n", e)}

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

使用make()初始化切片时, length长度最好设置为0, 否则会出现大量无效的值。

注意: append()函数只适合slice切片数据类型的追加。

s := make([]int, 5)

fmt.Println(s) //[0 0 0 0 0]

s = append(s, 6) //[0 0 0 0 0 6]

fmt.Println(s)

package main //必须有个main包import(    "fmt")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


copy

函数copy在两个slice间复制数据(也可以复制数组), 两个slice可指向同一底层数组。

func copy(dst, src []Type) int

array -> slice 复制

package main //必须有个main包import (    "fmt"    "reflect")func main() {    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]    fmt.Println(reflect.TypeOf(data), reflect.TypeOf(s2), reflect.TypeOf(s2)) //[10]int []int []int}

array -> slice

package main //必须有个main包import (    "fmt")func main() {    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(s1, s2) //dst:s1(目标), src:s2(源)    fmt.Println(s1) //[0 1]    fmt.Println(data) //[0 1 2 3 4 5 6 7 0 1]}

slice -> slice 复制

package main //必须有个main包import (    "fmt"    "reflect")func main() {    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]    fmt.Println(reflect.TypeOf(data), reflect.TypeOf(s2), reflect.TypeOf(s2)) //[]int []int []int}

汇总

package main //必须有个main包import ("fmt""reflect")func main() {/*同一底层的数组 此种在实际情况少见*/data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // 切片的定义方式// 第一种 目标的长度(dst) 大于 源(src) 且操作底层同一数组s1 := data[8:] //{8, 9}s2 := data[:5] //{0, 1, 2, 3, 4} 长度为5copy(s2, s1) //dst:s2(目标), src:s1(源) 本质上是操作底层同一个数组fmt.Println(s2) //[8 9 2 3 4] 长度与原目标的长度是一致 5, 源(src)全部覆盖目标(dst)fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9]fmt.Println(reflect.TypeOf(data), reflect.TypeOf(s2), reflect.TypeOf(s2)) //[]int []int []int// 第二种 目标的长度(dst) 小于 源(src) 且操作底层同一数组data2 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // 切片的定义方式s3 := data2[8:] //{8, 9} 长度为 2s4 := data2[:5] //{0, 1, 2, 3, 4}copy(s3, s4) //dst:s3(目标), src:s4(源) 本质上是操作底层同一个数组fmt.Println(s3) // [0 1] 长度与原目标的长度是一致 , 源(src)部分覆盖目标(dst)/*不同底层的数组, 在实际的案例比较多见*/// 第三种 目标的长度(dst) 小于 源(src) 且操作不同的数组s5 := []int{8, 9} // 长度为 2s6 := []int{0, 1, 2, 3, 4} // 定义不同底层的数组copy(s5, s6) //dst:s3(目标), src:s4(源) 如果 dst(目标) 长度小于 src(源) 的长度,  copy 源(src)部分;fmt.Println(s5) // [0 1] 长度与原目标的长度是一致 2, 源(src)部分覆盖目标(dst)// 第三种 目标的长度(dst) 大于 源(src) 且操作不同的数组s7 := []int{8, 9}s8 := []int{0, 1, 2, 3, 4} // 长度为 5copy(s8, s7) //dst:s8(目标), src:s7(源) 如果 dst(目标) 长度大于 src(源) 的长度,  copy 源(src)全部;fmt.Println(s8) // [8 9 2 3 4] 长度与原目标的长度是一致 5, 源(src)全部覆盖目标(dst)}

注意: copy("目标", "源"), 它只能用于切片, 不能用于 map 等任何其他类型;

它返回结果为一个 int 型值, 表示 copy 的长度;

源(src)不会发生变化(包括长度len和值), 而目标(dst)的长度len始终不会发生变化, 而目标的值被源(src)的值所覆盖, 具体覆盖多少(部分或全部)取决于目标(dst)的长度(len);

如果 dst(目标) 长度小于 src(源) 的长度, 则 copy 部分;

如果 dst(目标) 长度大于 src(源) 的长度, 则 copy 全部;

切片做函数参数

值传递

package main //必须有个main包import(    "fmt")func test(s []int){    s[0] = -1    fmt.Println("test : ")    s = append(s, 1000)    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,s[10] = 1000,    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,}/*test :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, s[10] = 1000,main :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,*/

提示: 我们发现采用值传递, 对切片进行修改操作, 执行的结果一致, 因为实际上切片操作是同一底层数组; 但是采用append()追加元素时, 返回的结果却不一致

引用传递

package main //必须有个main包import (    "fmt")func test(s *[]int) {(*s)[0] = -1*s = append(*s, 1000)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, s[10] = 1000,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)    }    //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, s[10] = 1000,}/*test :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, s[10] = 1000,main :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, slice[10] = 1000,*/

提示: 我们发现采用引用传递, 对切片进行修改操作, 执行的结果一致, 采用append()追加元素时, 返回的结果一致

注意:

slice 本质上是基于数组实现的, 当传参时, 函数接收到的参数是数组切片的一个复制, 虽然两个是不同的变量, 但是它们都有一个指向同一个地址空间的array指针;

当修改一个数组切片时, 另外一个也会改变, 所以数组切片看起来是引用传递, 其实是值传递;

append()扩容后则使用的不再是同一个内存地址, 因此需要采用引用传递;

结论

(1) append()扩容后则使用的不再是同一个内存地址

(2) 临时变量会开辟新的内存地址, 不会改变原有元素的值

(3) 切片作为函数的参数和返回值时, 一般的情况下没必要使用指针, 除非 append() 扩容的需要;

冒泡法排序切片

package main //必须有个main包import(    "fmt"    "math/rand"    "time")func InitData(s []int){    //设置种子    rand.Seed(time.Now().UnixNano())        for i := 0; i < len(s); i++ {            s[i] = rand.Intn(100) //100以内的随机数        }    }func BubbleSort(s []int){    n := len(s)    for i := 0; i < n; i++ {        for j := 0; j < n-1-i; j++ {            if( s[j] > s[j+1]){                s[j], s[j+1] = s[j+1], s[j]            }        }    }}func main(){    n := 10    //创建一个切片, len为n    s := make([]int, n)    InitData(s) //初始化数组    fmt.Println("排序前: ", s)    BubbleSort(s) //冒泡排序    fmt.Println("排序后: ", s)    }    /*    排学前: [93 7 8 94 41 36 16 71 63 25]    排学后: [7 8 16 25 36 41 63 71 93 94]*/

删除切片一个元素

Go语言并没有提供用于删除元素的语法或接口, 而是通过利用切片本身的特性来删除元素——追加元素。即以被删除元素为分界点, 将前后两个部分的内存重新连接起来。

使用切片的追加(append)特性, 利用代码实现。

Slice 删除元素的实现

由于切片没有语法糖实现删除, 因此利用其追加元素的特性完成元素的删除操作;通过内建函数 append() 实现对单个元素以及元素片段的删除。

具体的思路就是

确定删除位置 -> 连接起来 -> 原始切片 -> 将删除前后的元素 -> 产生连接后的切片

package mainimport (    "fmt")func main() {    // 初始化一个新的切片 seq    seq := []string{"a", "b", "c", "d", "e", "f", "g"}    // 指定删除位置    index := 3    // 输出删除位置之前和之后的元素    //fmt.Println(seq[:index], seq[index+1:])    // seq[index+1:]... 表示将后段的整个添加到前段中    // 将删除前后的元素连接起来    seq = append(seq[:index], seq[index+1:]...)    // 输出链接后的切片    fmt.Println(seq)}

遍历删除切片所遇到的坑

package mainimport ("fmt")func main() {    /*    // 最终报错panic: runtime error: index out of range [3] with length 3, 因为range在迭代时已经确定i的范围为[0,len(arr))的左闭右开的区间。    // 但是当满足arr[i] == 3 时对arr进行了修改, 缩短了arr的长度, 此时len(arr)=3, 最大下标为2, 因此当执行arr[3]时会报错。    // 使用 for...rang    arr := []int{1, 2, 3, 4}    for i := range arr {    if arr[i] == 3 {    arr = append(arr[:i], arr[i+1:]...)    }    }    fmt.Println(arr)    */    // 正确的写法    arr := []int{1, 2, 3, 4}    for i := 0; i < len(arr); i++ {        fmt.Println(i, arr[i])        if arr[i] == 3 { // 必须采用数组和+下标的方式修改, Go 和 PHP 语法一致            arr = append(arr[:i], arr[i+1:]...) // 删除数组的方式        i--   		 }    }    fmt.Println(arr)}

注意: 删除切片中的元素时, 要使用 for ... 循环;

尤其是切片的数量发生变化时, 不能使用 for...range 方式, 容易出现数组越界问题

计算切片中最大值

package mainimport ("fmt")func main() {    // 获取最大值    weights := []int{10, 30, 50, 70, 90, 110}    var max int    // var max int = weights[0]    for _, weight := range weights {    if weight > max {    max = weight    }    }    fmt.Println(max) // 110}

总结:

1. 切片:切片是数组的一个引用, 因此切片是引用类型。但自身是结构体, 值拷贝传递。

2. 切片的长度可以改变, 因此, 切片是一个可变的数组。

3. 切片遍历方式和数组一样, 可以用len()求长度。表示可用元素数量, 读写操作不能超过该限制。

4. cap可以求出slice最大扩张容量, 不能超出数组限制。0 <= len(slice) <= len(array), 其中array是slice引用的数组。

5. 切片的定义:var 变量名 []类型, 比如 var str []string var arr []int。

6. 如果 slice == nil, 那么 len、cap 结果都等于 0。

切片函数常见操作

1 将切片 b 的元素追加到切片 a 之后:a = append(a, b...)

2 复制切片 a 的元素到新的切片 b 上:

b = make([]T, len(a))

copy(b, a)

3 删除位于索引 i 的元素:a = append(a[:i], a[i+1:]...)

4 切除切片 a 中从索引 i 至 j 位置的元素:a = append(a[:i], a[j:]...)

5 为切片 a 扩展 j 个元素长度:a = append(a, make([]T, j)...)

6 在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]...)...)

7 在索引 i 的位置插入长度为 j 的新切片:a = append(a[:i], append(make([]T, j), a[i:]...)...)

8 在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, a[i:]...)...)

9 取出位于切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]

10 将元素 x 追加到切片 a:a = append(a, x)