写点什么

你真的理解 Golang 切片吗?全切片表达式及切片使用技巧

作者:宇宙之一粟
  • 2023-04-28
    中国香港
  • 本文字数:2822 字

    阅读完需:约 9 分钟

你真的理解 Golang 切片吗?全切片表达式及切片使用技巧

简介

Golang 中通常的 slice 语法是 a[low:high],您可能很熟悉。还有另一种切片语法,形式为 a[low:high:max],它采用三个索引而不是两个索引。第三索引 max 是做什么的?

提示: 不是 Python 切片语法 a[low:high:step] 中的 step 步长索引。


答: 第三个索引用于设置切片的容量!在 Golang 规范中称为 “全切片表达式”。

了解 Golang 切片


为了理解为什么要在 Golang 中加入这个功能,以及它的作用,让我们从数组和指针开始。


越界错误在 C 语言程序中很常见,Golang 通过内置的运行时边界检查器来缓解这个问题。数组的边界检查很简单,因为 Golang 的数组是固定长度的,然而,指针的边界检查就不那么简单了,因为指针的边界没有明确定义。Golang 中的切片只是解决指针的边界检查的一种方法。


Golang 不使用普通的指针来访问数组元素,而是用一个长度字段来扩充指针;结果(带长度的指针)被称为 "切片",或在其他地方称为 "胖指针"。有了长度字段,运行时的边界检查就很容易了。


Golang 的切片不仅仅是带长度的指针,它们还有一个 "容量 "字段,因为增加动态分配的数组是一个很常见的任务。分片的容量也作为分片表达式 a[low:high] 的边界检查器——切片的末端不能超过其容量。


理解 a[low:high:max]

切片索引表达式由长度字段进行边界检查,长度字段可以通过分片来减少,以提供所需的边界检查。

同样地,人们可能会想,是否有可能减少片断的容量,以加强对切片表达式 a[low:high] 的边界检查。例如,下面的表达式将一个切片的容量减少到它的长度:

a = a[0:len(a):len(a)]
复制代码

在这之后,切片 a 被限制在它自己的范围内,切片结束后的元素不能被访问或修改,即使你不小心重新分片或追加到它上面。


这个技巧对于从不可改变的数组中返回一个切片很有用;如果你不小心追加到所谓的不可改变的切片中,就会强制复制,并且没有数据被覆盖,因为已经没有容量了。


这种形式的分片表达式在 Golang 规范中被称为 "完整分片表达式"(full slice expression)。


切片的使用技巧

定义切片:

type SeriesInt64 struct {   values   []int64}
复制代码

自从引入内置的 append 以来,Go 1 中删除的 container/vector 包的大部分功能都可以使用 appendcopy 来复制。


自从引入泛型以来,golang.org/x/exp/slices 包中提供了其中几个函数的泛型实现。


以下是矢量方法及其切片操作:

AppendVector


a = append(a, b...)
复制代码



Copy

b := make([]T, len(a))copy(b, a)
// These two are often a little slower than the above one,// but they would be more efficient if there are more// elements to be appended to b after copying.b = append([]T(nil), a...)b = append(a[:0:0], a...)
// This one-line implementation is equivalent to the above// two-line make+copy implementation logically. But it is// actually a bit slower (as of Go toolchain v1.16).b = append(make([]T, 0, len(a)), a...)
复制代码


封装成函数,可以这样写:

func (s *SeriesInt64) copy() *SeriesInt64 {    if len(s.values) == 0 {        return &SeriesInt64{            values:[]int64{},        }    }    // Copy slice    x := s.values[0 : len(s.values)]    newSlice := append(x[:0:0], x...)    return &SeriesInt64{        values: newSlice,    }}
复制代码

Cut

a = append(a[:i], a[j:]...)
复制代码


Delete

a = append(a[:i], a[i+1:]...)// ora = a[:i+copy(a[i:], a[i+1:])]
复制代码


封装后:

func (s *SeriesInt64) remove(row int) {    s.values = append(s.values[:row], s.values[row+1:]...)}
复制代码

Delete without preserving order

a[i] = a[len(a)-1] a = a[:len(a)-1]
复制代码



NOTE 如果元素的类型是指针或带指针字段的结构体,需要进行垃圾回收,上述 CutDelete 的实现存在潜在的内存泄漏问题:一些有值的元素仍然被切片 a 引用,从而无法收集。下面的代码可以解决这个问题:

Cut

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]
复制代码



Delete

copy(a[i:], a[i+1:])a[len(a)-1] = nil // or the zero value of Ta = a[:len(a)-1]
复制代码



Delete without preserving order

a[i] = a[len(a)-1]a[len(a)-1] = nila = a[:len(a)-1]
复制代码



Expand

Insert n elements at position i:

a = append(a[:i], append(make([]T, n), a[i:]...)...)
复制代码


Extend

Append n elements:

a = append(a, make([]T, n)...)
复制代码



Extend Capacity

Make sure there is space to append n elements without re-allocating:

if cap(a)-len(a) < n {	a = append(make([]T, 0, len(a)+n), a...)}
复制代码

Filter (in place)

n := 0for _, x := range a {	if keep(x) {		a[n] = x		n++	}}a = a[:n]
复制代码


Insert

a = append(a[:i], append([]T{x}, a[i:]...)...)
复制代码



注意:第二个追加创建一个新的切片,它有自己的底层存储,并将 a[i:] 中的元素复制到该切片,然后这些元素被复制回切片 a(由第一个追加)。使用替代方法可以避免创建新切片(以及内存垃圾)和第二个副本:

Insert

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

封装后:

func (s *SeriesInt64) insert(row int, val int64) {    s.values = append(s.values, 0)    copy(s.values[row+1:], s.values[row:])    s.values[row] = val}
复制代码

InsertVector

a = append(a[:i], append(b, a[i:]...)...)
// The above one-line way copies a[i:] twice and// allocates at least once.// The following verbose way only copies elements// in a[i:] once and allocates at most once.// But, as of Go toolchain 1.16, due to lacking of// optimizations to avoid elements clearing in the// "make" call, the verbose way is not always faster.//// Future compiler optimizations might implement// both in the most efficient ways.//// Assume element type is int.func Insert(s []int, k int, vs ...int) []int { if n := len(s) + len(vs); n <= cap(s) { s2 := s[:n] copy(s2[k+len(vs):], s[k:]) copy(s2[k:], vs) return s2 } s2 := make([]int, len(s) + len(vs)) copy(s2, s[:k]) copy(s2[k:], vs) copy(s2[k+len(vs):], s[k:]) return s2}
a = Insert(a, i, b...)
复制代码

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:]
复制代码



Prepending

func (s *SeriesInt64) prepend(val int64) {    if cap(s.values) > len(s.values) {        s.values = s.values[:len(s.values)+1]        copy(s.values[1:], s.values)        s.values[0] = val        return    }    // No extra capacity so a new slice needs to be allocated:    s.insert(0, val)}
复制代码

图片来源:

发布于: 2023-04-28阅读数: 26
用户头像

宇宙古今无有穷期,一生不过须臾,当思奋争 2020-05-07 加入

🏆InfoQ写作平台-签约作者 🏆 混迹于江湖,江湖却没有我的影子 热爱技术,专注于后端全栈,轻易不换岗 拒绝内卷,工作于外企开发,弹性不加班 热衷分享,执着于阅读写作,佛系不水文 同名公众号:《宇宙之一粟》

评论 (1 条评论)

发布
用户头像
推荐推荐
2023-04-28 19:13 · 上海
回复
没有更多了
你真的理解 Golang 切片吗?全切片表达式及切片使用技巧_Go_宇宙之一粟_InfoQ写作社区