数组

和以往认知的数组有很大不同。

  • 数组是值类型,赋值和传参会复制整个数组,而不是指针。
  • 数组长度必须是常量,且是类型的组成部分。[2]int[3]int 是不同类型。
  • 支持 "=="、"!=" 操作符,因为内存总是被初始化过的。(因为是值类型,所以如果两个数组内元素顺序和值相同,则两个数组相同)
  • 指针数组 [n]*T,数组指针 *[n]T
a := [3]int{1,2,3}
b := [3]int{1,2,3}
fmt.Printf("%p %p\n",&a,&b)//0xc00000e380 0xc00000e3a0 地址位置不同
fmt.Println(a==b)//true

数组初始化

a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4:200} // 使用索引号初始化元素。

//使用符合语句初始化匿名结构体数组
d := [...]struct {
    name string
    age uint8
}{
    {"user1", 10}, // 可省略元素类型。
    {"user2", 20}, // 别忘了最后一行的逗号。
}

多维数组初始化

a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。

数组是值类型!

值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。

package main

import "fmt"

func test(x [2]int) {
    fmt.Printf("x: %p\n", &x)
    x[1] = 1000
}

func main() {
    a := [2]int{}
    fmt.Printf("a: %p\n", &a) //a: 0xc000018040

    test(a) //x: 0xc000018060 传入的是原数组的拷贝,而非之前的数组
    fmt.Println(a) //[0 0]
}

内置函数 lencap 都返回数组长度 (元素数量)。

切片

需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

//slice内部结构
struct Slice { // must not move anything
    byte* array; // actual data
    uintgo len; // number of elements
    uintgo cap; // allocated number of elements
};

切片的特点:

  • 引用类型。但自身是结构体,值拷贝传递。
  • 属性 len 表示可用元素数量,读写操作不能超过该限制。
  • 属性 cap 表示最大扩张容量,不能超出数组限制。
  • 如果 slice == nil,那么 lencap 结果都等于 0

切片与数组的关系

data := [...]int{0, 1, 2, 3, 4, 5, 6}
slice := data[1:4:5] // [low : high : max]

通过数组创建切片

使用语法 数组名[low:high:max]来创建切片,规律如下:

对于数组:data := [...]int{0,1,2,3,4,5,6,7,8,9}

切片表达式 切片内容 切片长度len 切片容量cap 备注
data[:6:8] 0 1 2 3 4 5 6 8 省略 low
data[5:] 5 6 7 8 9 5 5 省略high、max
data[:3] 0 1 2 3 10 省略low、max
data[:] 0 1 2 3 4 5 6 7 8 9 10 10 全部省略
    data := [...]int{1,2,3,4,5}
    s1 := data[:]
    data[2] = 100
    fmt.Println(s1) //[1 2 100 4 5]

可见使用这种方法创建的切片的数据数组就是原数组

其实这种方法实际使用不多

reslice,用切片创建切片

这种方法在代码上和上一种很相似

所谓 reslice,是基于已有 slice 创建新 slice 对象,以便在 cap 允许范围内调整属性。

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := s[2:5] // [2 3 4]
s2 := s1[2:6:7] // [4 5 6 7]
s3 := s2[3:6] // Error

新对象依旧指向原底层数组。

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := s[2:5] // [2 3 4]
s1[2] = 100

s2 := s1[2:6] // [100 5 6 7]
s2[3] = 200

fmt.Println(s) //[0 1 2 3 100 5 6 200 8 9]

直接创建切片

直接创建 slice 对象,自动分配底层数组。

s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。

s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。

s3 := make([]int, 6) // 省略 cap,相当于 cap = len。

使用 make 动态创建 slice,避免了数组必须用常量做长度的麻烦,在实际使用中,这种创建切片的方式更为常用

切片的读写操作

和使用数组一样,但需要注意索引号的差别,即切片和数组的下标是相互独立的

data := [...]int{0, 1, 2, 3, 4, 5}

s := data[2:4] // {2,3,4}
s[0] += 100 // {102,3,4}
s[1] += 200 // {102,203,4}

fmt.Println(s) // {102,203,4}
fmt.Println(data) // {0,1,102,203,4,5}

当然也可以用指针直接访问底层数组

s := []int{0, 1, 2, 3}

p := &s[2] // *int, 获取底层数组元素指针。
*p += 100

s[2] += 100 //也可以直接使用数组的方式操作

可直接修改 struct array/slice 成员。

d := [5]struct {
    x int
}{}

s := d[:]

d[1].x = 10
s[2].x = 20

fmt.Println(d) // [{0} {10} {20} {0} {0}]
fmt.Printf("%p, %p\n", &d, &d[0]) // 0x20819c180, 0x20819c180

二(多)维切片

与多维数组一样 [][]T,是指元素类型为[]T的切片

data := [][]int{
    []int{1, 2, 3},
    []int{100, 200},
    []int{11, 22, 33, 44},
}

切片的常用方法

append

func append(slice []Type, elems ...Type) []Type

slice 尾部添加数据,返回新的 slice 对象。

s := make([]int, 0, 5)
fmt.Printf("%p\n", &s) //0x210230000

s2 := append(s, 1)
fmt.Printf("%p\n", &s2) //0x210230040

fmt.Println(s, s2) //[] [1]

说明append前后的两个切片不是同一个切片

    //如果 cap 够用(此处cap为5,但只放入了5个元素)
    s1 := make([]int, 0, 5)
    s1 = append(s1,1)
    fmt.Printf("%p\n", &s1[0]) //0xc0000c8030

    s2 := append(s1, 2)
    fmt.Printf("%p\n", &s2[0]) //0xc0000c8030

    //如果 cap 不够用(此处cap为1,但放入了两个元素)
    s1 := make([]int, 0, 1)
    s1 = append(s1,1)
    fmt.Printf("%p\n", &s1[0])//0xc0000a0068

    s2 := append(s1, 2)
    fmt.Printf("%p\n", &s2[0])//0xc0000a00a0

可见:

​ 如果cap够用,则append前后使用的是相同的数据数组

​ 如果cap不够用,则append前后使用不同的数据数组

注意:是否使用新的数据数组与数据数组容量无关,之和切片的cap有关,所以会出现即便数据数组没满但超过cap限制导致重新分配数据数组

最佳实践

通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。

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

func copy(dst, src []Type) int

函数 copy 在两个 slice 间复制数据

srcSlice,其中 srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice)

复制长度以 len 小的为准,函数的返回值就是复制的长度。

两个 slice 可指向同一底层数组,允许元素区间重叠。

package main

import "fmt"

func main() {
    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

    s := data[8:] //[8 9] 
    s2 := data[:5]// [0 1 2 3 4]

    copyLen := copy(s2, s) // dst:s2, src:s 从s往s2复制,复制长度为2

    fmt.Println(copyLen) //2
    fmt.Println(s)//[8 9]
    fmt.Println(s2)//[8 9 2 3 4]
    fmt.Println(data)//[8 9 2 3 4 5 6 7 8 9]
}

参考

go 边看边练

Go语言copy():切片复制(切片拷贝)


此 生 无 悔 恋 真 白 ,来 世 愿 入 樱 花 庄 。