一、 数组
1.array的定义
- 定义数组的格式:
var a[4]int // 数组中元素自动初始化为类型对应的零值
a := [...]int{19:1} // 编译器按照初始化值数量确定数组长度
a := [5]int{1,2} // 未提供初始值的,数组中元素自动初始化为类型对应的零值
a[0] = 3 // 数组的访问
- 数组长度也是类型的一部分,因此具有不同长度的数组为不同类型
- 数组是值类型
2.数组指针和指针数组
// 数组指针是指获取数组变量的地址。
// 此时变量 p 就是指向数组的指针。特别注意 p 定义的类型为长度为100的数组的指针。 长度必须相等才能赋值。
func main() {
var a = [...]int{99:1}
var p *[100]int = &a
fmt.Println(p)
}
// 指针数组是指元素为指针类型的数组
func main() {
var x, y = 2, 3
var a = [...]*int{&x, &y}
fmt.Println(a)
*a[0] = 20 // 指针数组赋值
*a[1] = 30
}
在 go 中数组是值类型,所以可以用在赋值操作中。前提是数组长度和类型都相同。
// 声明第一个包含 5 个元素的字符串数组
var array1 [5]string
// 声明第二个包含 5 个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 把 array2 的值赋值给 array1
array1 = array2
fmt.Println(array1)
fmt.Println(array2)
输出:
[Red Blue Green Yellow Pink]
[Red Blue Green Yellow Pink]
如果是指针类型的数组赋值操作了:
// 声明第一个包含 3 个元素的指向字符串的指针数组
var array1 [3]*string
// 声明第二个包含 3 个元素的指向字符串的指针数组
// 使用字符串指针初始化这个数组
array2 := [3]*string{new(string), new(string), new(string)}
// 使用颜色为每个元素赋值
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
// 将 array2 复制给 a
array1 = array2
fmt.Println(array1)
fmt.Println(array2)
输出:
[0xc0000661e0 0xc0000661f0 0xc000066200]
[0xc0000661e0 0xc0000661f0 0xc000066200]
可以看到,实际上是把数组的指针赋值到了另一个数组里面,而不是赋值的指针指向的值。所以修改任一数组中指针指向的值都会相互影响。
3.数组之间的比较
如果数组的元素类型支持 == 或 != ,那么数组之间也可以使用 == 或 != 进行比较,但不可以使用 < 或 >
//数组类型必须相同才能比较
func main() {
a := [2]int{1,2}
b := [2]int{1,3}
fmt.Println(a == b)
}
4.使用 new 创建数组,此方法返回一个数组指针
func main() {
p := new([10]int)
fmt.Println(p)
}
5.多维数组
func main() {
a := [2][3]int{
{1,2,3},
{4,5,6}
}
b := [2][3]int{
{1:1},
{2:2}
}
c := [...][3]int{ // 仅允许第一维度使用 ... 的表示
{1:1},
{2:2}
}
}
6.在函数中传递数组
在函数间传递数组开销是很大的。在 go 中,函数间参数属于值传递(除 map、slice、chan等)。所以如果传入的是一个数组,那么不管数组有多长,都会 copy 一份传递给函数。解决办法就是通过传递数组的指针,不过要注意此时数组在函数里面的修改会影响其本身。
二、切片
切片是一种数据结构,它是围绕动态数组的概念来构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。还可以通过对切片再次切片(reslice)来缩小一个切片的大小。
slice 在源码中的结构如下:
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 长度(元素的个数)
cap int // 容量(允许放入的元素个数)
}
1.slice 概述
- 其本身并不是动态数组或数组指针,它内部通过指针引用底层数组
- 作为变长数组的替代方案,可以关联底层数组的局部或全部
- 是引用类型
- 可以直接创建或从底层数组获取生成
- 使用
len()获取元素个数,cap()获取容量
2.创建 slice
使用 make() 创建或使用切片字面量
// 使用:make([]T, len, cap)
// len 便是存数的元素个数,cap 表示容量
s1 := make([]int, 3, 6) // 指定len、cap,底层数组初始化为零值, len 不能大于 cap
s2 := make([]int, 3) // 省略cap,和len相等
s3 := []int{1,2,3,4} // 通过字面量创建
s4 :=[]int{1, 2, 5:3} // 按初始化元素分配底层数组,并设置 len、cap,[1 2 0 0 0 3]
s5 := []int{99:1} // 指定长度和容量都是100
nil切片和空切片
var s1 []int // nil切片
s2 := make([]int, 0) // 空切片
s3 := []int{} // 空切片
不管是nil切片还是空切片,对其调用内置函数 append、len、cap 效果都是一样的。
3.reslice 概述(slice的切片操作)
- Reslice时索引以被slice的切片为准
- 索引不可以超过被slice的切片的容量cap()值
- 索引越界不会导致底层数组的重新分配而是引发错误
- 新建的切片对象仍然指向原底层数组
s1 := []int{1,2,3,4,5}
s2 := s1[1:] // reslice操作
我们来看一个例子:
// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3] // reslice:其长度为2,容量为4
fmt.Println(newSlice) // 输出:[20 30]
此时 slice 和 newSlice 共享同一段底层数组,但是不同的切片看到的是底层数组不同的部分。newSlice 引用的元数组的一部分。如下图:
对底层数组容量是 k 的切片 slice[i:j] 来说:
长度:j - i
容量:k - i
接下我们修改一下值:
newSlice[1] = 35
fmt.Println(slice)
fmt.Println(newSlice)
输出:
[10 20 35 40 50]
[20 35]
你会发现对 newSlice 的操作居然会影响到原来的 slice,由此也能证明 newSlice 和原 slice 引用的是同一个底层数组。
切片只能访问到其长度内的元素,否则会 panic:
newSlice[3] = 55 // newSlice 上面已经提过长度为 2
输出:
panic: runtime error: index out of range [3] with length 2
这里其实还有个容易让人糊涂的地方是:newSlice 的长度是2,但是容量却是 4,但是了又不能访问到超过长度的部分。我们接着来看一个操作:
newSlice = append(newSlice, 3, 2)
fmt.Println(slice)
fmt.Println(newSlice)
输出:
[10 20 35 3 2]
[20 35 3 2]
append 的功能是切片的扩容,把元素3、2追加到原来的切片后面。此时你会发现原来的 slice 最后两个元素变成了3和2,因为 newSlice 和 slice 引用的同一个底层数组。
4.append
相对于数组,适用切片的好处就是,可以按需增加切片容量,使用内置函数 append 就可以了。
在切片后追加一个元素:
s:= []int{1,2}
s = append(s,3}
fmt.Println(s) // [1,2,3]
在切片后追加另一个切片的元素:
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s1 = append(s1, s2...) // 注意这里使用了"..."
fmt.Println(s2) // [1,2,3,4,5,6]
如果最终长度未超过追加到slice的容量则返回原始slice:
s := []int{1, 2, 3, 4, 5}
ns := s[1:3]
ns = append(ns, 1)
ns[0] = 11
fmt.Println(s, ns)
输出:
[1 11 3 1 5] [11 3 1]
s 的长度是5,容量是5,而 ns 的长度是2容量是4。所以 ns = append(ns, 1) 后,ns 的长度变为3,未超过容量4,所以 append 后返回的还是原来的 slice。所以这里 ns[0] = 11 修改操作也会影响到原来的 s。
如果超过追加到的slice的容量则将重新分配数组并拷贝原始数据
s := []int{1, 2, 3, 4, 5}
ns := s[1:3]
ns = append(ns, 10, 20, 30)
ns[0] = 11
fmt.Println(s, len(s), cap(s))
fmt.Println(ns, len(ns), cap(ns))
输出:
[1 2 3 4 5] 5 5
[11 3 10 20 30] 5 8
可以看到 ns 执行 append 后,由于长度超过了容量,所以 append 会创建一个新的底层数组,将被引用的现有的值复制到新的数组里,再追加新的值。
同时注意此时 ns 的容量变为了原来的2倍。当然如果当容量变大以后,就不一定2倍了。
5.常用的一些操作:
s := []int{1, 2, 3, 4, 5}
s = append(s, 6) // 在 s 后追加元素
s = append([]int{10}, s...) // 在 s 前添加一个元素
fmt.Println(s)
s = s[:len(s)-1] // 拿掉最后一个元素
fmt.Println(s)
s = s[1:] // 拿掉第一个元素
fmt.Println(s)
输出:
[10 1 2 3 4 5 6]
[10 1 2 3 4 5]
[1 2 3 4 5]
这样就可以模拟一些出栈、入栈、出队、入队的相关操作
5.copy() 与slice
//此处表示a拷贝进b里面,所以b变为1,2,3
func main() {
a := []int{1,2,3,4}
b := []int{5,6,7}
copy(b, a)
fmt.Println(a, b)
}
输出:
[1 2 3 4] [1 2 3]
//把a的第一个元素,拷贝到b的第二个元素上
func main() {
a := []int{1,2,3,4}
b := []int{5,6,7}
copy(b[1:2], a[0:1])
fmt.Println(a, b)
}
输出:
[1 2 3 4] [5 1 7]