复合类型
数组 Array
概念(某百科)
数组(Array)是有序的元素序列。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便,把具有相同类型的若干元素按有序的形式组织起来的一种形式。这些有序排列的同类数据元素的集合称为数组。数组是用于储存多个相同类型数据的集合。
数组分为==定长数组==和==可变长数组==,这里我们重点说明的是定长数组,因为Go中可变长数组是另一种类型-slice(切片)。
定长数组 注意点
- 数组:是同一种数据类型的固定长度的序列。
- 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
- 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
- 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
- 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
- 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
- 支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
- 指针数组 [n]*T,数组指针 *[n]T。
- 由于为定长数组所以len和cap返回值相同。
- 不同长度的数组为不同的数组,不能互相拷贝
- 因为长度不同,会导致短数组下标越界
数组遍历形式
- for i := 0; i < len(a); i++ {}
- for index, v := range a {}
推荐使用第二种,可以避免数组越界;
数组的灵活用法
- 另类循环
var times [5][0]int
for range times {
fmt.Println("hello")
}
var arr = [4]int{1,2,3,4}
fmt.Println(unsafe.Sizeof(arr)) //Stdout 32
fmt.Println(unsafe.Sizeof(times)) //Stdout 0 所占内存为0
- 不占空间的数组还可以用来做通道的值
- 因为不占空间,因此通道传递过程中减少了赋值操作
说明 times对应的一个数组[5][0]int类型的数组,虽然第一维有长度,但是数组的元素[0]int大小是0,因此整个数组占用的内存大小依然为0。不用付出额外的内存代价,我理解这样写法上也较为简洁。
//数组声明及使用
var arr = [4]int{1,2,3,4}
fmt.Println(arr)
Stdout
[1 2 3 4]
fmt.Println(arr[5]); //直接这样使用 - 编译器可提示错误
//测试下标越界
for i := 0; i <= 5; i++ {
fmt.Println(arr[i])
}
Stdout
1
2
3
4
panic: runtime error: index out of range [4] with length 4
//值拷贝测试
func testCopyArr(arr []int) {
fmt.Printf("%p \n", &arr)
}
func main() {
//数组内存值拷贝测试
var ar = []int{1,2,3,4}
fmt.Printf("%p \n", &ar)
testCopyArr(ar)
}
Stdout
0xc0000a6020
0xc0000a6040
//内存地址发生变化了,因此证明testCopyArr中的数组并不是main中的数组,而是复制之后的新数组。
// 数组比较
var a = [3]int{1,2,3}
var b = a
fmt.Printf("%b", a == b)
Stdout
%!b(bool=true)
//数组遍历
var a = []int{1,2,3,4}
for i := range a {
fmt.Printf("a[%d]: %d \n", i, a[i])
}
for i,v := range a {
fmt.Printf("a[%d]: %d \n", i, v)
}
for i := 0; i < len(a); i++ {
fmt.Printf("a[%d]: %d \n", i, a[i])
}
Stdout
略
以上数组,均以int数组为例。
其他数组形式
//字符数组
var s1 = [2]string{"hello", "world"}
var s2 = [...]string{"你好", "世界"}
var s3 = [...]string{0:"你好", 1:"世界"}
//结构体数组
var struct1 [2]image.Point
var struct2 = [...]image.Point{{0,0}, {1,1}}
// 函数数组
var funcArr1 [2]func(io.Reader) (image.Image, error)
var funcArr2 = [...]func(io.Reader) (image.Image, error) {
png.Decode,
jpeg.Decode,
}
// 接口数组
var unknown1 [2]interface{}
var unknown2 = [...]interface{}{123, "你好"}
// 通道数组
var chanList = [2]chan int{}
切片 - slice
概念
简单理解,slice就是一个动态数组(可变长度数组)。所以slice并不是数组或数组指针。它是通过内部指针和相关属性引用数组片段,以实现变长的方案。
可变长数组 - 注意点
- 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
- 切片的长度可以改变,因此,切片是一个可变的数组。
- 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
- cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
- 切片的定义:var 变量名 []类型,比如 var str []string var arr []int。
- 如果 slice == nil,那么 len、cap 结果都等于 0。
遍历切片的方式
同数组
切片截取说明
注: low,high 遵循前包后不包原则
切片操作
- 添加 append
- 概念
- append本质是为了==追加元素==,并不是为了扩容(只是追加的==副作用==)
- 容量不足时,append操作会导致重新分配内存,可能导致巨大的内存分配和复制数据的代价。
- 插入方式
- 头部插入
- 尾部插入
- 在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制一次。
- 链式操作
- 多次append会造成临时生成切片
- 利用copy和append结合避免临时生成切片
- 概念
- 删除
- 本质是移动切片的low或者high
//切片使用
var a []int //nil 切片;和 nil 相等
fmt.Printf("%b \n", a == nil) //%!b(bool=true)
var b = []int{} // 空切片 和 nil 不相等
fmt.Printf("%b \n", b == nil) // %!b(bool=false)
var c = []int{1,2,3} // 3个元素的切片 len cap 都是3
fmt.Printf("len: %d; cap: %d \n", len(c), cap(c)) //len: 3; cap: 3
var d = c[:2] // 有2个元素的切片 len 2 cap 3
fmt.Printf("len: %d; cap: %d; %v \n", len(d), cap(d), d) //len: 2; cap: 3 [1 2]
var e = c[0:2] // 有两个元素的切片 len 2 cap 3
fmt.Printf("len: %d; cap: %d; %v \n", len(e), cap(e), e) //len: 2; cap: 3 [1 2]
var f = c[:0] // 有两个元素的切片 len 0 cap 3
fmt.Printf("len: %d; cap: %d; %v \n", len(f), cap(f), f) //len: 0; cap: 3 []
var g = make([]int, 3) // 有三个元素的切片 len = 3 cap = 3 初始化为 0 0 0
fmt.Printf("len: %d; cap: %d; %v \n", len(g), cap(g), g) //len: 3; cap: 3 [0 0 0]
var h = make([]int, 2, 3) // 有三个元素的切片 len = 2 cap = 3 初始化为 0 0
fmt.Printf("len: %d; cap: %d; %v \n", len(h), cap(h), h) //len: 2; cap: 3 [0 0]
var i = make([]int, 0, 3) // 有三个元素的切片 len = 0 cap = 3
fmt.Printf("len: %d; cap: %d; %v \n", len(i), cap(i), i) //len: 0; cap: 3 []
var arr = []int{1,2,3,4}
fmt.Println(arr)
// 切片添加
var a []int
// 尾部插入
a = append(a, 1)
fmt.Printf("%v \n", a) // [1]
a = append(a, 1, 2, 3)
fmt.Printf("%v \n", a) // [1 1 2 3]
a = append(a, []int{1,2,3}...)
fmt.Printf("%v \n", a) // [1 1 2 3 1 2 3]
// 头部插入
a = append([]int{0}, a...)
fmt.Printf("%v \n", a) // [0 1 1 2 3 1 2 3]
a = append([]int{-3,-2,-1}, a...)
fmt.Printf("%v \n", a) // [-3 -2 -1 0 1 1 2 3 1 2 3]
// 链式操作
a = append(a[:0], append([]int{0}, a...)...)
fmt.Printf("%v", a) // [0 -3 -2 -1 0 1 1 2 3 1 2 3]
// 避免生成临时切片,但是代码写的实在是有点冗长……,实际应用中应该用不到
i := 0
var b = []int{1,2,3}
b = append(b, 0)
copy(b[i+1:], b[i:])
b[i] = 1
fmt.Printf("%v \n", b) //[1 1 2 3]
//删除元素
var a = []int{1,2,3,4}
a = a[:len(a) - 1]//删除尾部第一个元素
fmt.Printf("%v \n", a) //[1 2 3]
a = a[1:] //删除头部第一个元素
fmt.Printf("%v \n", a) //[2 3]
注
- Go语言实现中非0大小数组的长度不得超过2GB,因此需要针对数组元素的类型大小计算数组的最大长度范围([]uint8最大2GB,[]uint16最大1GB,依此类推,
- []struct{}数组的长度可以超过2GB