[TOC]
2 复合数据类型
2.1 数组
数组特点:
- 声明之后长度固定不可修改
- 数组首元素地址就是数组地址
- 每个元素之间的地址都是根据数据类型的占位来有序扩张。如 int 占用 8 字节,那么他的下一个元素会在上一个元素的地址基础上 +8
- 数组先在内存根据类型和 index 的总数计算出所需空间,并开辟内存空间,在将每个 index 的元素放入
数组的访问
- 通过 index 访问
- 首元素
arr[0]
- 末元素
arr[len(arr)-1]
,因为在数组中 index 是从 0 开始计算第一个元素
- 首元素
- 访问二维数组里的元素
- 位于第三行第四列的元素
arr[2][3]
- 位于第三行第四列的元素
数组的遍历
// 数组遍历有以下几种方法:
// 通过 for-range 遍历数组里的元素
for i, ele := range arr {
fmt.Printf("index=%d, element=%d\n", i, ele)
}
// for-i 或者这样遍历数组
for i := 0; i < len(arr); i++ { //len(arr)获取数组的长度
fmt.Printf("index=%d, element=%d\n", i, arr[i])
}
// 遍历二维数组
for row, array := range arr { //先取出某一行
for col, ele := range array { //再遍历这一行
fmt.Printf("arr[%d][%d]=%d\n", row, col, ele)
}
}
cap 和 len
- cap 代表 capacity 容量
- len 代表 length 长度
- len 代表目前数组里的几个元素,cap 代表给数组分配的内存空间可以容纳多少个元素
- 由于数组初始化之后长度不会改变,不需要给它预留内存空间,所以
len(arr)==cap(arr)
数组传参
- 数组的长度和类型都是数组类型的一部分,函数传递数组类型时这两部分都必须吻合
- go 语言没有按引用传参,全都是按值传参,即传递数组实际上传的是数组的拷贝,当数组的长度很大时,仅传参开销都很大
- 如果想修改函数外部的数组,就把它的指针(数组在内存里的地址)传进来
2.1.1 数组初始化
数组是块连续的内存空间,在声明的时候必须指定长度,且长度不能改变。所以数组在声明的时候就可以把内存空间分配好,并赋上默认值,即完成了初始化。
如上图中有一个数组元素分别是 2,9,7,3,5
在内存中是一整块连续存放的,数组在声明的时候必须指定长度如:arr := [5]int{2,9,7,3,5}
这是声明并赋值,而且每个 int 类型占用 8 字节,然后该数组有 5 个 index ,5 * 8 = 40 byte
数组声明及赋值几种方式
var arr1 [5]int = [5]int{} // 数组必须指定长度和类型,且长度和类型指定后不可改变,没有赋值任何元素默认 5 个 0
var arr2 = [5]int{}
var arr3 = [5]int{3, 2} // 给前2个元素赋值,后面 3 个元素为 0
var arr4 = [5]int{2: 15, 4: 30} // 指定 index 赋值,这里是给下标 2,4 赋值 15,30
var arr5 = [...]int{3, 2, 6, 5, 4} // 根据{}里元素的个数推断出数组的长度为 5 因为这里只赋值了 5 个
var arr6 = [...]struct {
name string
age int
}{{"Tom", 18}, {"Jim", 20}} // 数组的元素类型由匿名结构体给定
arr := [5]int{1, 2, 3, 4, 5} // 通过类型推导赋值
2.1.2 二维数组
// 5 行 3 列,只给前 2 行赋值,且前 2 行的所有列还没有赋满
// arr1 数组长度为 5 并且每个 index 的元素为一个长度为 3 的一维数组
var arr1 = [5][3]int{{1}, {2, 3}}
// 第1维可以用 ... 推测,第 2 维不能用 ...
var arr2 = [...][3]int{{1}, {2, 3}}
2.1.2.1 二维数组赋值
package main
import "fmt"
func main() {
// 二维数组赋值
// 一维数组 [3] 表示在该数组中有 3 个大括号
// 二维数组 [5] 表示在一维数组的大括号中有 5 个元素
var arr2 = [3][5]int{{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15}}
fmt.Println(arr2)
}
输出
[15:53:31 root@go day2]#go run main.go
[[1 2 3 4 5] [6 7 8 9 10] [11 12 13 14 15]]
2.1.2.1 二维数组遍历
package main
import "fmt"
func main() {
var arr2 = [3][5]int{{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15}}
fmt.Println("for-range 遍历")
// for-range 遍历
for _, v := range arr2 {
for _, v2 := range v {
fmt.Print(v2)
}
}
fmt.Println()
fmt.Println("for-i 遍历")
// for-i 遍历
for i := 0; i < len(arr2); i++ {
for j := 0; j < len(arr2[i]); j++ {
fmt.Print(arr2[i][j])
}
}
fmt.Println()
}
输出
[16:08:52 root@go day2]#go run main.go
for-range 遍历
123456789101112131415
for-i 遍历
123456789101112131415
2.1.3 数组代码演示
2.1.3.1 数组的赋值演示
package main
import "fmt"
func main() {
// 没有对 arr1 和 arr2 数组进行赋值
var arr1 [5]int = [5]int{}
var arr2 = [5]int{}
// 对 arr3 前 4 个元素赋值
var arr3 = [5]int{1, 2, 3, 4}
// 指定对 index 1 和 index4 赋值为 3,5
var arr4 = [5]int{1: 3, 4: 5}
// 通过推到赋值
var arr5 = [...]int{1, 2, 3}
fmt.Println(arr1)
fmt.Println(arr2)
fmt.Println(arr3)
fmt.Println(arr4)
fmt.Println(arr5)
}
输出
# 可以看到对没有赋值的数组元素默认值为 0
[14:47:27 root@go day2]#go run main.go
[0 0 0 0 0]
[0 0 0 0 0]
[1 2 3 4 0]
[0 3 0 0 5]
[1 2 3]
2.1.3.2 数组元素访问演示
package main
import "fmt"
func main() {
var arr1 = [5]int{1, 2, 3, 4, 5}
// 访问下标为 2 的元素
fmt.Println(arr1[2])
}
输出
[14:53:40 root@go day2]#go run main.go
3
访问最后一个元素范例
package main
import "fmt"
func main() {
var arr1 = [5]int{1, 2, 3, 4, 5}
// 访问最后一个元素
fmt.Println(len(arr1) - 1)
}
输出
[14:56:28 root@go day2]#go run main.go
4
2.1.3.3 数组元素修改演示
package main
import "fmt"
func main() {
var arr1 = [5]int{1, 2, 3, 4, 5}
// 对 arr1 数组的 index=4 的元素进行赋值 50
arr1[4] = 50
fmt.Println(arr1)
}
输出
# arr1 的 index=4 值变为了 50
[14:55:42 root@go day2]#go run main.go
[1 2 3 4 50]
2.1.3.4 数组指针演示
因为我在上面说过了数组的地址和该数组的第一个元素地址是一样的
package main
import "fmt"
func main() {
a := [5]int{1, 2, 3}
// 去数组的地址
fmt.Printf("%p\n", &a)
// 去数组首元素的地址
fmt.Printf("%p\n", &a[0])
}
输出
# 能够看到第一个元素的地址就是该数组的地址
[14:36:58 root@go day2]#go run main.go
0xc0000a0030
0xc0000a0030
2.1.3.5 数组遍历
package main
import "fmt"
func main() {
var arr1 = [5]int{1, 2, 3, 4, 5}
// for-i 遍历
for i := 0; i < len(arr1); i++ {
fmt.Println(i, ":", arr1[i])
}
fmt.Println()
// for-range 遍历
for i, v := range arr1 {
fmt.Println(i, v)
}
}
输出
[15:13:26 root@go day2]#go run main.go
0 : 1
1 : 2
2 : 3
3 : 4
4 : 5
0 1
1 2
2 3
3 4
4 5
2.1.3.6 数组求平均值范例
package main
import "fmt"
func avg(arr [5]int) float64 {
sum := 0
for i := 0; i < len(arr); i++ {
sum = sum + arr[i]
}
return float64(sum / len(arr))
}
func main() {
var arr1 = [5]int{1, 2, 3, 4, 5}
fmt.Println(avg(arr1))
}
输出
[15:10:22 root@go day2]#go run main.go
3
2.1.3.7 跨函数数组修改
因为数组是 copy 类型,如果我们想跨函数对数组进行修改的话,就得通过传递指针类型,从而使得其他函数也是在对该数组的同一块内存空间进行操作
package main
import "fmt"
func Arrfunc(arr [5]int) {
// 这里修改了 arr 数组 index=1 的元素为 200
arr[0] = 200
fmt.Println("Arrf:", arr)
fmt.Printf("%p\n", &arr)
}
func main() {
arr := [5]int{1, 2, 3, 4, 5}
Arrfunc(arr)
fmt.Println("main:", arr)
fmt.Printf("%p\n", &arr)
}
输出
# 可以看到在 Arrf 函数中 arr 数组值已经被修改,但是在 main 函数中并没有修改,因为 Arrf 和 main 两个函数是两块独立的内存空间
[15:47:27 root@go day2]#go run main.go
Arrf: [200 2 3 4 5]
0xc0000a0060 # 并且两个函数中的 arr 内存地址也不匹配
main: [1 2 3 4 5]
0xc0000a0030
在上面案例中并没有实现对 arr 数组在其他函数的修改,如果想实现在其他函数对同一个数组进行修改并赋值的话就需要使用到指针通过传递地址来实现
package main
import "fmt"
// * 指针解引用
func Arrfunc(arr *[5]int) {
// 这里修改了 arr 数组 index=1 的元素为 200
arr[0] = 200
fmt.Println("Arrf:", arr)
fmt.Printf("%p\n", arr) // 这里 arr 已经是指针类型所以不用在加 & 取地址
}
func main() {
arr := [5]int{1, 2, 3, 4, 5}
// & 取 arr 地址传到 Arrfunc 函数中
Arrfunc(&arr)
fmt.Println("main:", arr)
fmt.Printf("%p\n", &arr)
}
输出
# 可以看到 index=1 的元素已经在 main 函数中修改
[15:53:16 root@go day2]#go run main.go
Arrf: &[200 2 3 4 5]
0xc000024090 # 地址匹配成功
main: [200 2 3 4 5]
0xc000024090
2.2 切片
如上图 array 指向一块数组的内存地址 cap=5 表示这个切片要存放 5 个元素,同时我的 len 为 3 ,也就是说实际上该切片的前三个才是我真正的元素,虽然 index 4 和 index 5 不是我的元素,但是先将其占住,表示后期会使用
如果在切片上面调用指针的话,取到的就是 array 对应数组的地址
切片本身是一个结构体,并且该结构体中有三个属性,既然这个 slice 有这么三个属性,那么这三个属性分别表示什么意思:
- array:指针指向一块数组的内存空间,并指向该数组的首地址
- len:表示当前切片所使用的元素个数
- cap:表示当前切片需要预留的内存空间
// 切片源码
type slice struct {
array unsafe.Pointer // 指针类型 unsafe.Pointer
len int
cap int
}
2.2.1 初始化切片
声明一个切片和声明一个数组非常相识,差别就是在声明数组的时候需要指定数组长度,而切片不需要
var s []int //切片声明,len=cap=0
s = []int{} //初始化,len=cap=0
s = make([]int, 3) //初始化,len=cap=3
s = make([]int, 3, 5) //初始化,len=3,cap=5
s = []int{1, 2, 3, 4, 5} //初始化,len=cap=5
s2d := [][]int{
{1},{2, 3}, //二维数组各行的列数相等,但二维切片各行的len可以不等
}
// 在声明切片的时候可以通过 make 现在在内存中开辟一块空间
2.2.1.1 二维初始化范例
package main
import "fmt"
func main() {
// 定义二维切片
s2d := [][]int{
{1}, {23, 1,2},
}
fmt.Printf("s2dlen = %v\nvalue = %v\n", len(s2d), s2d)
fmt.Println()
fmt.Printf("s2d[0]len = %v\nvalue = %v\n", len(s2d[0]), s2d[0])
fmt.Println()
fmt.Printf("s2d[1]len = %v\nvalue = %v\n", len(s2d[1]), s2d[1])
}
输出
[17:28:59 root@go day2]#go run main.go
s2dlen = 2
value = [[1] [23 1]]
s2d[0]len = 1
value = [1]
# 因为在 s2d[1] 元素的切片中元素有 3 个,所以长度为 3
s2d[1]len = 3
value = [23 1 2]
2.2.1.2 一维初始化范例
package main
import "fmt"
func main() {
// 以下就是切片的几种初始化
var slice1 []int = []int{}
var slice2 = []int{}
// 通过 make 初始化
var slice3 = make([]int, 2)
var slice4 = make([]int, 3, 4)
// 短声明
slice5 := []int{}
slice6 := make([]int, 2)
slice7 := make([]int, 3, 4)
fmt.Printf("len=%d,cap=%d\n", len(slice1), cap(slice1))
fmt.Printf("len=%d,cap=%d\n", len(slice2), cap(slice2))
fmt.Printf("len=%d,cap=%d\n", len(slice3), cap(slice3))
fmt.Printf("len=%d,cap=%d\n", len(slice4), cap(slice4))
fmt.Printf("len=%d,cap=%d\n", len(slice5), cap(slice5))
fmt.Printf("len=%d,cap=%d\n", len(slice6), cap(slice6))
fmt.Printf("len=%d,cap=%d\n", len(slice7), cap(slice7))
}
输出
[17:36:16 root@go day2]#go run main.go
len=0,cap=0
len=0,cap=0
len=2,cap=2
len=3,cap=4
len=0,cap=0
len=2,cap=2
len=3,cap=4
2.2.2 append
- 切片相对于数组最大的特点就是可以追加元素,可以自动扩容
- 追加的元素放到预留的内存空间里,同时 len 加1
- 如果预留空间已用完,则会重新申请一块更大的内存空间,capacity大约变成之前的2倍(cap<1024)或1.25倍(cap>1024)。把原内存空间的数据拷贝过来,在新内存空间上执行 append 操作
2.2.2.1 append 原理
切片 append 操作的底层原理分析:
- 切片 append 操作本质就是对切片扩容
- go 底层会创建一个新的数组 newArr(安装扩容后大小),也就是说在执行 append 操作的时候其实会创建一个新的数组。会把这个新的数组扩容之后从新赋给这个新的切片。
- 将 slice 原来包含的元素拷贝到新的数组中 newArr(也就是会先创建一个新的数组,这个新的数组大小呢就是按照扩容后的大小来进行创建,然后再把这个准备追加的切片的内容拷贝到这个新的数组里面去)
- 接着 slice 重新引用到这个 newArr 中去
- 注意 newArr 是在底层来维护的,程序员不可见。(也就是说 append 如何在底层扩容这个数组我们是不用去考虑的)
append 操作的底层原理总结:
- 如上图:在内存中 slice3 切片有三个内容,分别是数组首元素地址,len = 3,cap = 3 ,数组分别对应的是 100、200、300。
- 当代码执行到
slice3 = append(slice3 ,400 ,500 ,600)
的时候 append 会在内存中开辟一块新的空间,这时候这个 append 空间中的有六个数组元素空间默认值都为 0 ,接着再把 slice 3 切片中的内容拷贝到 append 这个新的内存空间中,也就是将 slice3 的 100,200,300 拷贝到 append 的空间中。接着再将 400,500,600 同样赋值到 append 内存空间中。 - 这时候 append 就生成了一个新的数组元素为 100,200,300,400,500,600。
- 此时我们将 append 的返回值赋值给 slice3 这个切片,这时候 slice3 的数组元素就指向了 100,200,300,400,500,600。
- 然后原先的 slice3 在内存中的空间就会被作为垃圾回收(因为没有任何变量引用这块空间)。并且 len 变化为了 6 cap 容量也变成了 6
append 指向新的变量底层原理总结:
如上图:关键是看 append 过后将返回的这个数据交给那个变量,如果这时候交给了一个新的变量 slice4 ,新的变量 slice4 就会指向 append 在内存中的空间,原先的这个 slice3 就没有变化
2.2.2.2 append 添加追加元素
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4}
fmt.Println("追加前", s)
s = append(s, 5, 6, 7, 8)
fmt.Println("追加后", s)
}
输出
[17:42:24 root@go day2]#go run main.go
追加前 [1 2 3 4]
追加后 [1 2 3 4 5 6 7 8]
2.2.3 make 扩容
通过 make 演示
package main
import "fmt"
func main() {
// 这里定义了 s 切片的长度和容量都是 3
s := make([]int, 3, 3)
fmt.Printf("len=%d , cap=%d , value=%d\n", len(s), cap(s), s)
// 然后对 s 切片追加了 100 ,此时按理说 s 切片就应该装不下了
s = append(s, 100)
fmt.Printf("len=%d , cap=%d , value=%d\n", len(s), cap(s), s)
}
输出
[17:46:39 root@go day2]#go run main.go
len=3 , cap=3 , value=[0 0 0]
len=4 , cap=6 , value=[0 0 0 100]
# 通过输出可以看到追加之后长度变为了 4 容量在原有的 3 上面扩了一倍变为了 6
# 由于我们没有赋值给该切片,所以初始默认值就为 0
如果上面的案例还不够清楚那么下面我在通过代码演示
package main
import "fmt"
func cap_slice() {
s := make([]int, 3, 5)
// 取出当前的 s 切片容量
prevCap := cap(s)
// 循环 100 次
for i := 0; i < 100; i++ {
// 并对 s 切片增加 0
s = append(s, 0)
// 而且当 s 重新 append 之后在取出 s 切片的容量给 currcap
currCap := cap(s)
// 如果当 prevCap 上一次的 s 容量小于 currCap 就将其输出,并将 prevCap = currCap
if prevCap < currCap {
fmt.Printf("cap %d --> %d\n", prevCap, currCap)
prevCap = currCap
}
}
}
func main() {
cap_slice()
}
输出
[17:58:20 root@go day2]#go run main.go
cap 5 --> 10
cap 10 --> 20
cap 20 --> 40
cap 40 --> 80
cap 80 --> 160
# 通过输出可以看到 cap 都是成两倍的增加,如果当前的 cap 不够,就会重新扩充一倍的 cap
2.2.4 截取子切片
- 刚开始,子切片和母切片共享底层的内存空间,修改子切片会反映到母切片上,在子切片上执行append会把新元素放到母切片预留的内存空间上
- 当子切片不断执行 append ,耗完了母切片预留的内存空间,子切片跟母切片就会发生内存分离,此后两个切片没有任何关系
- go 在截取子切片中取前不取后
s := make([]int, 3, 5) //len=3, cap=5
sub_slice = s[1:3] //len=2, cap=4 在 sub_slice 中的 cap 就是截取的 s 切片中的 cap
// 如上代码,原本在 s 切片中有 3 元素,然后 s[1:3] 将 1,2 两个下标的元素截取出来而不会取出 3 的下标,并且这个时候 sub_slice 和 s 共享底层的内存空间,因为切片的第一个元素是一个指针并指向一个数组,而这个数组被 s 和 sub_slice 所共享
// 也就是说当我们修改了 sub_slice 为 0 的 index 元素,间接也会修改到 s 中对应该 index 的元素,同样的修改了 s 之后 sub_slice 也会生效,因为共享底层的内存空间而切片是引用类型所以会互相影响
// 如果说在 sub_slice 中调用 append 的话,会直接 append 到 s 预留的内存空间中,假如我们在 sub_slice 切片中不断的调用 append 直到将 s 切片的预留空间用完了,sub_slice 就会在指向一块新的内存空间,这个时候 s 还是指向原来的那块内存空间,这时 s 和 sub_slice 切片的内存空间就完全独立互不影响
如上图通过:
- make 创建一个
cap=5 len=3
的 arr 切片,然后 crr 截取arr[:2]
也就是 index 为 0,1 -
并且 crr 和 arr 是共享 同一块内存空间的,接着将
crr[1]
元素修改为 8 ,此时如果输出arr[1]
得到的元素也同样是 8 -
接着往 crr 中
append 4
,此时 arr 和 crr 的第三个元素值就为 4 - 然后反复的指向
crr = append(crr,4)
- 如果当 arr 中的 cap 容量不够了,但是 crr 还想 append ,这是 crr 就会去申请一块新的内存空间,并会将 crr 中的全部元素 copy 到新的内存空间中,而 arr 依然使用老的内存空间,crr 就指向了新的内存空间
2.2.4.1 子父切片共享内存空间验证
1.共享内存空间验证
package main
import "fmt"
func sub_slice() {
// 默认值为 0
arr := make([]int, 3, 5)
// 前闭后开截取,包含 0 index 不包含 2 index,所以 crr len=2
crr := arr[:2]
// 将 crr[1] 元素修改为 8
crr[1] = 8
// 将 arr 输出观察是否受影响
fmt.Println("arr :", arr)
// 接着追加一个 9 到 crr 中,这是 crr[2] = 9,并且会将 9 也赋值到 arr[3] 中
crr = append(crr, 9)
fmt.Println("crr :", crr)
fmt.Println("arr :", arr)
}
func main() {
sub_slice()
}
输出
[18:58:07 root@go day2]#go run main.go
arr 默认值: [0 0 0]
arr : [0 8 0] # 通过输出发现 arr[1] 元素已经改变,从而验证子切片共享父切片内存地址
crr : [0 8 9] # 给 crr = append(crr, 9) 之后长度增加至 3 ,第 3 个元素 = 9
arr : [0 8 9] # 并且 arr 父切片也会被影响
但是我刚才说了如果说子切片在不断 append 并且超出了父切片的 cap 容量,这时候他们两个的内存空间就会分离
2.内存空间分离验证
package main
import "fmt"
func sub_slice() {
// 默认值为 0
arr := make([]int, 3, 5)
fmt.Println("arr 默认值:", arr)
// 前闭后开截取,包含 0 index 不包含 2 index
crr := arr[:2]
// 将 crr[1] 元素修改为 8
crr[1] = 8
// 将 arr 输出观察是否受影响
fmt.Println("arr :", arr)
fmt.Printf("内存分离前地址: %p,%p\n", &arr[0], &crr[0])
// 接着追加一个 9 到 crr 中,这是 crr[2] = 9,并且会将 9 也赋值到 arr[3] 中
crr = append(crr, 9, 2, 3, 4)
fmt.Println("crr :", crr)
fmt.Println("arr :", arr)
fmt.Printf("内存分离后地址: %p,%p\n", &arr[0], &crr[0])
}
func main() {
sub_slice()
}
输出
[22:10:03 root@go day2]#go run main.go
arr 默认值: [0 0 0]
arr : [0 8 0]
内存分离前地址: 0xc000024090,0xc000024090
crr : [0 8 9 2 3 4]
arr : [0 8 0]
内存分离后地址: 0xc000024090,0xc00007e000
# 通过输出只要当 crr 的 append 长度超出了父切片,就会出现内存空间分离
# 并且内存分离前地址都是相同的,直到内存分离后我们可以看到 crr[0] 的地址就发生了变化
2.2.5 切片传参
go 语言函数传参都是值拷贝,传的都是值,即传切片会把切片的{arrayPointer, len, cap}
这3个字段拷贝一份传进来。
由于传的是底层数组的指针,所以可以直接修改底层数组里的元素
比如下面代码中我创建一个函数,因为我们都知道在数组中跨函数的话会是一个值拷贝类型,而我这里想验证的是切片他是一个引用类型,也就是说能够实现跨函数访问同一块内存地址
package main
import "fmt"
func slice(s []int) {
s[0] = 100
fmt.Printf("slice:prt=%p , value=%d\n", &s[0], s)
}
func main() {
s := []int{1, 2, 3, 4}
slice(s)
fmt.Printf("main:prt=%p , value=%d\n", &s[0], s)
}
输出
# 通过输出可以看到不论是元素的首地址还是说两个函数中的切片值都是一样的,因为他们两个函数都是在对同一块内存空间进行操作
# 而且传切片的话不需要对值进行拷贝操作,从而性能比数组提示不少
[22:34:42 root@go day2]#go run main.go
slice:prt=0xc0000180a0 , value=[100 2 3 4]
main:prt=0xc0000180a0 , value=[100 2 3 4]