这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战
目录:
- 【手把手教你写Go】01.为什么要选择Go语言
- 【手把手教你写Go】02.hello world
- 【手把手教你写Go】03.基本数据类型
- 【手把手教你写Go】04.运算符和流程控制
- 【手把手教你写Go】05.函数
- 【手把手教你写Go】06.工程管理
- 【手把手教你写Go】07.复合类型-指针
- 【手把手教你写Go】08.复合类型-数组和切片
- 【手把手教你写Go】08.复合类型-map
- 【手把手教你写Go】09.复合类型- 结构体
- 【手把手教你写Go】10. 面向对象编程
- 【手把手教你写Go】11. 异常处理
- 【手把手教你写Go】12. 文本处理
- 【手把手教你写Go】13. 并发编程- 并发入门
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一样,只是不需要长度。
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}
| 操作 | 结果 | 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 |
| array[:] | [0 1 2 3 4 5 6 7 8 9] | 10 | 10 | 全部省略 |
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
*/
}
- 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]
}
所以可以看出,切片其实是通过指针,取的底层数组的一部分数据。
提示:可以从数组中定义切片,也可以从切片中定义新的切片。