9.Go 切片

切片是相同类型的值的可增长序列。

其他语言称它们为数组或向量。

slice使用的内存由固定大小的数组提供。 切片是对该数组的视图。

切片具有长度和容量。

容量表示一个切片可以包含的元素总数,也就是底层数组的大小。

长度是切片中已有元素的数量。

容量和长度的差别在于,在必须重新分配底层数组之前,可以在一个切片上附加多少个元素。

切片的零值为nil。

slice := make([]int, 0, 5)
// append element to end of slice
slice = append(slice, 5)
// append multiple elements to end
slice = append(slice, 3, 4)
fmt.Printf("length of slice is: %d\n", len(slice))
fmt.Printf("capacity of slice is: %d\n", cap(slice))

length of slice is: 3
capacity of slice is: 5

创建 slice

大多数时候,不必显式创建切片。切片的零值为nil,append的值为零:

var a []int
fmt.Printf("a is %#v\n", a)
a = append(a, 3)
fmt.Printf("a is %#v\n", a)

a is []int(nil)
a is []int{3}

创建空切片的两种方法:

var nilSlice []bool
empty1 := []bool{}
empty2 := make([]bool, 0)
fmt.Printf("nilSlice is %#v\n", nilSlice)
// 空slice和nil slice不同
fmt.Printf("empty1 is %#v\n", empty1)
fmt.Printf("empty2 is %#v\n", empty2)

nilSlice is []bool(nil)
empty1 is []bool{}
empty2 is []bool{}

空slice和nil slice不同

创建静态预分配的切片:

a := []int{3, 1, 4, 1}
fmt.Printf("a has %d elements\n", len(a))

a has 4 elements

创建全是0的静态预分配的切片:

a := make([]string, 4)
fmt.Printf("a has %d elements\n", len(a))

a has 4 elements

如果知道切片的预期大小,则可以为其内容预分配空间,这有助于提高性能:

n := 30
a := make([]int, 0, n)
fmt.Printf("a has lenght %d and capacity %d\n", len(a), cap(a))
for i := 0; i < n; i++ {
    a = append(a, i)
}
fmt.Printf("a has lenght %d and capacity %d\n", len(a), cap(a))

a has lenght 0 and capacity 30
a has lenght 30 and capacity 30

长度和容量

切片具有长度和容量, 容量表示一个切片可以包含的元素总数,也就是底层数组的大小,长度是切片中已有元素的数量。

当使用make()函数创建slice时,可以指定其长度和容量:

var s = make([]int, 3, 5)
fmt.Printf("Length:   %d\n", len(s))
fmt.Printf("Capacity: %d\n", cap(s))

Length: 3
Capacity: 5

如果未明确指定容量,它将默认为指定的长度的值。

var s = make([]int, 4)
fmt.Printf("Length:   %d\n", len(s))
fmt.Printf("Capacity: %d\n", cap(s))

Length: 4
Capacity: 4

由make()创建的slice,其元素设置为其零值:

var s = make([]int, 3)
for idx, val := range s {
    fmt.Println(idx, val)
}

0 0
1 0
2 0

即使索引小于容量,也不能访问超出切片长度的元素:

s := make([]int, 3, 20)
fmt.Println(s[5])

panic: runtime error: index out of range [5] with length 3

goroutine 1 [running]:
main.main()
/tmp/src554923030/main.go:9 +0x1d
exit status 2

追加slice

内置函数append(slice,elements ...)将元素添加到切片中并返回新的切片

追加一个值

a := []string{"hello"}
a = append(a, "world")
fmt.Printf("a: %#v\n", a)

a: []string{"hello", "world"}

追加多个值

a := []string{"hello"}
a = append(a, "world", "now")
fmt.Printf("a: %#v\n", a)

a: []string{"hello", "world", "now"}

追加slice到另一个slice

a := []string{"!"}
a2 := []string{"Hello", "world"}
a = append(a, a2...)
fmt.Printf("a: %#v\n", a)

a: []string{"!", "Hello", "world"}

append()会生成一个新的slice

slice使用固定大小的数组进行存储。当slice的元素数量超过数组的大小时,append需要重新分配数组并创建一个新的slice。 为了提高效率,新分配的数组有一些备用元素,因此不会在每次将元素追加到slice时都重新分配底层数组。

该程序显示重新分配新数组需要多少元素:

var a []int
ptr := fmt.Sprintf("%p", a)
n := 0
nAppends := 0
for {
    a = append(a, 1)
    nAppends++
    currPtr := fmt.Sprintf("%p", a)
    if currPtr != ptr {
        fmt.Printf("Appends needed to re-allocate slice: %d\n", nAppends)
        nAppends = 0
        ptr = currPtr
        n++
        if n == 6 {
            break
        }
    }
}

Appends needed to re-allocate slice: 1
Appends needed to re-allocate slice: 1
Appends needed to re-allocate slice: 1
Appends needed to re-allocate slice: 2
Appends needed to re-allocate slice: 4
Appends needed to re-allocate slice: 8

可见,重新创建数组所需的追加数量随slice的长度而增加。

Go不允许比较切片,因此我们使用以下事实:在当前实现中,我们可以获取slice的地址(%p),并且该地址可以用作slice的标识。

这是Go的内部实现将来可能会更改。

过滤slice

Go没有用于过滤切片的通用功能,因此我们必须采用老式的方法:

  1. 为结果创建一个新的切片
  2. 遍历原始切片的所有元素
  3. 检查是否应保留或拒绝元素
  4. 如果应保留,则附加到结果
func filterEvenValues(a []int) []int {
    var res []int
    for _, el := range a {
        if el%2 == 0 {
            continue
        }
        res = append(res, el)
    }
    return res
}

a := []int{1, 2, 3, 4}
res := filterEvenValues(a)
fmt.Printf("%#v\n", res)

[]int{1, 3}

我们可以通过重新使用切片的底层数组来更有效地执行此操作,因为我们知道结果将始终小于原始切片:

a := []int{1, 2, 3, 4}
res := filterEvenValuesInPlace(a)
fmt.Printf("%#v\n", res)

[]int{2, 4}

这样可以避免为结果分配新的数组,但也更加危险。
原始切片a现在已被修改,但从代码来看并不太明显。

从slice中删除元素

我们在示例中使用[] int,但是代码可用于所有类型的切片。

删除单个元素

s := []int{10, 11, 12, 13}

i := 2 // index of 12
s = append(s[:i], s[i+1:]...)

fmt.Printf("s: %#v\n", s)

s: []int{10, 11, 13}

删除多个元素

s := []int{10, 11, 12, 13}

i := 1 // index of 11
n := 2 // remove 2 elements

s = append(s[:i], s[i+n:]...)

fmt.Printf("s: %#v\n", s)

s: []int{10, 13}

清除效率
关于效率的注意事项:Go编译器足够聪明,可以重复使用原始切片中的空间,因此此方法非常有效。 它不会分配新的空间,而只是复制切片中的元素。 我们可以验证一下:

s := []int{10, 11, 12, 13}
fmt.Printf("&s[0]: %p, cap(s): %d\n", &s[0], cap(s))
fmt.Printf("s: %#v\n", s)

i := 1 // index of 11
n := 2 // remove 2 elements

s = append(s[:i], s[i+n:]...)

fmt.Print("\nAfter removal:\n")
fmt.Printf("&s[0]: %p, cap(s): %d\n", &s[0], cap(s))
fmt.Printf("s: %#v\n", s)

s = append(s, 1, 2, 3, 4)
fmt.Printf("\nAfter appending beyond remaining capacity:\n")
fmt.Printf("&s[0]: %p, cap(s): %d\n", &s[0], cap(s))
fmt.Printf("s: %#v\n", s)

&s[0]: 0xc00007e020, cap(s): 4
s: []int{10, 11, 12, 13}

After removal:
&s[0]: 0xc00007e020, cap(s): 4
s: []int{10, 13}

After appending beyond remaining capacity:
&s[0]: 0xc000088000, cap(s): 8
s: []int{10, 13, 1, 2, 3, 4}

%p formatting指令将物理地址输出到变量的内存中。 我们可以验证s指向相同的物理内存,并且在删除前后具有相同的容量,因此可以验证基础数组。

我们还可以看到,追加4个元素(超出了2的剩余容量)会导致数组重新分配。

优化的就地清除

如果我们不在乎保留元素的顺序,则可以进一步优化移除:

s := []int{10, 11, 12, 13}

i := 1 // index of 11
lastIdx := len(s) - 1

s[i] = s[lastIdx]
s = s[:lastIdx]

fmt.Printf("s: %#v\n", s)

s: []int{10, 13, 12}

我们用切片中的最后一个元素覆盖要删除的元素,并将切片缩小1。

与将所有元素从i复制到切片末尾相比,这将复制单个元素。 在小切片中这无关紧要,但是如果您有包含数千个元素的切片,则速度会更快

复制slice

一种选择是分配与原始切片相同长度的新切片,并使用copy()方法:

src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)

src: []int{1, 2, 3}
dst: []int{1, 2, 3}

另一种选择是将()原始切片追加到一个空切片:

src := []int{1, 2, 3}
dst := append([]int(nil), src...)

src: []int{1, 2, 3}
dst: []int{1, 2, 3}

两种方法都同样有效。

slice的初始值

切片的初始值为nil。

nil切片的长度和容量为0。

nil切片没有底层数组。

非nil切片的长度和容量也可以为0,例如[] int {}或make([] int,5)[5:]。

任何具有nil值的类型都可以转换为nil slice:

s := []int(nil)
fmt.Printf("s: %#v\n", s)

测试切片是否为空,请检查len是否为0:

s := []int(nil)
if len(s) == 0 {
    fmt.Printf("s  is empty: %#v\n", s)
}

var s2 []int
if len(s2) == 0 {
    fmt.Printf("s2 is empty: %#v\n", s2)
}

s3 := make([]int, 0)
if len(s2) == 0 {
    fmt.Printf("s3 is empty: %#v\n", s3)
}

Slice技巧

以下是slice的更多操作:

附加完整的slice

a = append(a, b...)

拷贝

b = make([]T, len(a))
copy(b, a)
// or
b = append([]T(nil), a...)
// or
b = append(a[:0:0], a...)

剪切

a = append(a[:i], a[j:]...)

删除

a = append(a[:i], a[i+1:]...)
// or
a = a[:i+copy(a[i:], a[i+1:])]

删除但不保留顺序

a[i] = a[len(a)-1] 
a = a[:len(a)-1]

注意如果元素的类型是指针或具有指针字段的结构(需要进行垃圾回收),则上述Cut和Delete的实现存在潜在的内存泄漏问题:某些具有值的元素仍由slice a引用,因此 无法收集。 以下代码可以解决此问题:

copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k < n; k++ {
    a[k] = nil // or the zero value of T
}
a = a[:len(a)-j+i]

删除

copy(a[i:], a[i+1:])
a[len(a)-1] = nil // or the zero value of T
a = a[:len(a)-1]

删除但不保留顺序

a[i] = a[len(a)-1]
a[len(a)-1] = nil
a = a[:len(a)-1]

扩展

a = append(a[:i], append(make([]T, j), a[i:]...)...)
//
a = append(a, make([]T, j)...)

插入

a = append(a[:i], append([]T{x}, a[i:]...)...)

注意第二个追加将创建一个具有其自身基础存储的新切片,并将a [i:]中的元素复制到该切片,然后将这些元素复制回切片a(由第一个追加)。 可以通过使用另一种方法来避免新切片的创建(以及由此产生的内存垃圾)和第二个副本:

插入

s = append(s, 0 /* use the zero value of the element type */)
copy(s[i+1:], s[i:])
s[i] = x

插入到位置i

a = append(a[:i], append(b, a[i:]...)...)

Push

a = append(a, x)

Pop

x, a = a[len(a)-1], a[:len(a)-1]

Push Front/Unshift

a = append([]T{x}, a...)

Pop Front/Shift

x, a = a[0], a[1:]

其他技巧

过滤而不分配

此技巧利用了一个事实,即切片与原始切片共享相同的后备阵列和容量,因此存储已重新用于过滤的切片。 当然,原始内容会被修改。

b := a[:0]
for _, x := range a {
    if f(x) {
        b = append(b, x)
    }
}

对于必须被垃圾回收的元素,随后可以包含以下代码:

for i := len(b); i < len(a); i++ {
    a[i] = nil // or the zero value of T
}

翻转

for i := len(a)/2-1; i >= 0; i-- {
    opp := len(a)-1-i
    a[i], a[opp] = a[opp], a[i]
}

或者:

for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 {
    a[left], a[right] = a[right], a[left]
}

打乱顺序
Fisher–Yates算法:

从go1.10开始,可以在math / rand上使用。

for i := len(a) - 1; i > 0; i-- {
    j := rand.Intn(i + 1)
    a[i], a[j] = a[j], a[i]
}

以最少的分配进行批处理
如果要对大型切片执行批处理,则很有用。

actions := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
batchSize := 3
var batches [][]int

for batchSize < len(actions) {
    actions, batches = actions[batchSize:], append(batches, actions[0:batchSize:batchSize])
}
batches = append(batches, actions)

产生以下内容:
[[0 1 2] [3 4 5] [6 7 8] [9]]

就地重复数据删除(可比)

import "sort"

in := []int{3,2,1,4,3,2,1,4,1} // any item can be sorted
sort.Ints(in)
j := 0
for i := 1; i < len(in); i++ {
    if in[j] == in[i] {
        continue
    }
    j++
    // preserve the original data
    // in[i], in[j] = in[j], in[i]
    // only set what is required
    in[j] = in[i]
}
result := in[:j+1]
fmt.Println(result) // [1 2 3 4]

通过预分配slice进行优化

相对而言,分配是昂贵的。

如果新大小超过切片的当前容量,则将新数据追加到切片需要重新分配内存。

知道了这一点,如果知道分片的最终大小,则可以预先分配所需的容量,并避免重新分配。

你不需要知道确切的大小:知道上限也是一样。猜测总比没有好。

考虑附加到空切片:

s := []byte("123456")
var d []byte
fmt.Printf("d: %p, len: %d, cap: %d\n", d, len(d), cap(d))
d = append(d, s...)
fmt.Printf("d: %p, len: %d, cap: %d\n", d, len(d), cap(d))
d = append(d, s...)
fmt.Printf("d: %p, len: %d, cap: %d\n", d, len(d), cap(d))
d = append(d, s...)
fmt.Printf("d: %p, len: %d, cap: %d\n", d, len(d), cap(d))

d: 0x0, len: 0, cap: 0
d: 0xc000016070, len: 6, cap: 8
d: 0xc000016090, len: 12, cap: 16
d: 0xc0000180a0, len: 18, cap: 32

结果可能会有所不同,具体取决于编译器的版本和运行时。 在Go 1.12中,4次将6个字节附加到一个空slice中需要32次分配。

通过用%p打印slice的地址,我们可以查看是否重新分配了它。

如上限所示,首先分配8个字节,然后将其重新分配给16个字节,最后分配给32个字节。

在每次重新分配时,必须复制内容,因此我们总共复制了6 + 6 * 2 + 6 * 3个字节。 要存储24个字节,我们浪费了复制36个字节的时间。

内存分配和复制内存需要时间。

Go运行时很聪明,它使容量增加了一倍,并预计将来我们可能需要更多空间。

我们可以比这更聪明,预先分配切片:

s := []byte("123456")
d := make([]byte, 0, len(s)*4)
fmt.Printf("d: %p, len: %d, cap: %d\n", d, len(d), cap(d))
d = append(d, s...)
fmt.Printf("d: %p, len: %d, cap: %d\n", d, len(d), cap(d))
d = append(d, s...)
fmt.Printf("d: %p, len: %d, cap: %d\n", d, len(d), cap(d))
d = append(d, s...)
d = append(d, s...)
fmt.Printf("d: %p, len: %d, cap: %d\n", d, len(d), cap(d))

d: 0xc00007e020, len: 0, cap: 24
d: 0xc00007e020, len: 6, cap: 24
d: 0xc00007e020, len: 12, cap: 24
d: 0xc00007e020, len: 24, cap: 24

在这里,我们可以看到我们从一开始就以正确的容量开始,并且运行库在任何时候都无需进行额外的分配或内存复制。

你可能感兴趣的:(9.Go 切片)