Go 学习笔记2 - 数组切片 |Go主题月

393 阅读8分钟

一、 数组

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 引用的元数组的一部分。如下图: image.png

对底层数组容量是 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]