likes
comments
collection
share

聊一聊 slice

作者站长头像
站长
· 阅读数 73

数组

在开始介绍切片之前需要先介绍一下 go 中的数组。数组是一块连续的存储空间, 定义了存储的类型和长度。下面是是声明长度为 3 的 int 数组, 初始值为 0. 数组可以直接用来比较, 当元素相同时, 返回 ture. 对于数组越界访问, 直接会编译报错.

初始化

聊一聊 slice

var arr [3]int            // [0, 0, 0]
arr1 := [...]int{0, 0, 0} // [0, 0, 0]
fmt.Println(a == cc)    //比较
// a[3] = 0 Invalid array index '3' (out of bounds for the 3-element array)

传参

当数组作为参数传递时, 传递的是数组的一个 copy, 更新操作并不会修改原数组

func main() {
   arr := [3]int{1, 2, 3}
   ArrParam(arr)
   fmt.Println(arr) // [1 2 3]
}

func ArrParam(arr [3]int) {
   arr[0] = 0
}

切片

切片是对数组的封装, 使用切片可以灵活的对数组进行扩容和裁剪。当我们使用数组时, 如果需要对数组进行追加操作, 需要先创建一个新的数组, 同时进行赋值操作。使用切片的话, 只通过一个 append 函数就可以实现追加操作。

切片底层是一个结构体, 里面包含了指向连续内存空间的指针, 已使用的长度, 申请空间的总长度。

聊一聊 slice 初始化

    var s []int // 声明一个切片
    fmt.Println(s == nil) // true
    s1, s2 := []int{1}, []int{1, 2, 3}
    //fmt.Println(s1 == s2) Invalid operation: s1 == s2 (the operator == is not defined on []int)
    fmt.Println(s1, len(s1), cap(s1)) // [1] 1 1
    fmt.Println(s2, len(s2), cap(s2)) // [1 2 3] 3 3
    s4 := make([]int, 1)
    fmt.Println(s4, len(s4), cap(s4)) //[0] 1 1
    s5 := make([]int, 1, 2)
    fmt.Println(s5, len(s5), cap(s5)) //[0] 1 2

扩容

在使用 make 声明一个数组时, 我们可以指定要申请的空间大小「cap」。例如:s5 := make([]int, 1, 2),申请了一块容量为 3 的存储空间, 使用的容量为 2, 最后一块空间并没有使用。

聊一聊 slice

s5 := make([]int, 1, 3)
fmt.Println(s5, len(s5), cap(s5)) // [0] 1 3

为什么需要支持显性的指定申请的切片容量呢?

有些业务场景, 需要对数组进行扩容。例如, 我们需要在 s5 后面追加一个 int 3。通过提前申请指定的容量, 可以避免重新申请内存, 从而提升运行速度。下面是一个对数组进行追加的例子, 使用 s5 可以直接修改对应的值, 同时更新 len; 使用 s6 的形式, 需要先申请容量为 3 的内存, 然后进行拷贝和赋值。

s5 := make([]int, 2, 3)
s5 = append(s5, 1)
fmt.Println(s5, len(s5), cap(s5))
s6 := make([]int, 2)
s6 = append(s6, 1)
fmt.Println(s6, len(s6), cap(s6))

聊一聊 slice

使用 append 函数进行数组追加时, 底层会先判断当前切片的容量是否满足, 如果不满足将申请新的存储空间。下面一个例子, 说明了扩容机制对于程序的影响。核心要区分清楚, 数组追加的时候是否需要进行重新申请内存空间。

s := []int{1}
s1 := s[0:1]
s2 := append(s, 1) // s2 指向新的内存空间
s1[0] = 0
fmt.Println(s, s2) // [0], [1,1]

s3 := []int{1, 2}
s3 = append(s3, 1)                // s3 分配一段 len:3, cap:4 的内存空间
fmt.Println(s3, len(s3), cap(s3)) // [1 2 1], 3, 4
s4 := s3[0:1]
s5 := append(s3, 1) // 使用现有的内存空间, 更新 len
s4[0] = 0           // s3[0] 也会被修改
fmt.Println(s4, s5) // [0], [0 2 1 1]

赋值

数组和切片是支持通过下标访问的。在 go 语言中, 可以通过 slice[left:right:cap] 的形式访问一段空间, 对应的含义是访问下标从 left - right 的左闭右开的空间, cap 可选, 可以指定容量下标。在切片赋值时, 相当于新建一个 slice, 底层还是共享同一块存储空间, 这样可以减少内存的分配与复制, 但也会有一些坑。例如下面的例子, 更新完 s 之后, s1 会同步被更新

s := []int{1, 2, 3}
s1 := s[0:2] // [1, 2], 与 s 有相同的指向
fmt.Println(s1, len(s1), cap(s1)) // [1, 2], 2, 3
s[0] = 0
fmt.Println(s1) // [0,2]

聊一聊 slice

如果我们期望不使用同一块存储空间, 需要对切片进行单独修改, 可以使用内置的 copy 函数, 显性的对切片进行拷贝

s := []int{1, 2, 3}
s1 := make([]int, 2)
copy(s1, s) // 赋值从 s1 的 0 下标开始, 到 min(len(s1), len(s)) 接口
s[0] = 0 // 不影响 s1 的数据
fmt.Println(s1, s) // [0,2], [0 2 3]

聊一聊 slice

传参

切片作为参数传递时, 其实是值传递。将 slice 看作一个结构体 struct{point, len, cap}, 参数传递时, 传递的是对象的值拷贝。从下面的例子, 我们可以看到, 函数里面进行扩容并不影响原切片「如果函数更改了指向数组的值, 原切片也会受影响」

func main() {
   s := []int{1, 2, 3}
   SliceParam(s)
   fmt.Println(s)          // [1 2 3]
   s1 := make([]int, 3, 4) // [0 0 0]
   SliceParam(s1)
   fmt.Println(s1) // [0 0 0]
}

func SliceParam(s []int) {
   s = append(s, 1)
}

如果我们想让函数更新传入的切片, 可以使用指针。下面的两个图, 说明了传递指针和传递参数的区别。

func main() {
   s := []int{1, 2, 3}
   SliceParam(&s)
   fmt.Println(s)          // [1 2 3 1]
   s1 := make([]int, 3, 4) // [0 0 0 1]
   SliceParam(&s1)
   fmt.Println(s1) // [0 0 0]
}

func SliceParam(s *[]int) {
   *s = append(*s, 1)
}

聊一聊 slice

快捷操作

func main() {
   s := []int{0, 1, 2, 3, 4, 5}
   // 增加
   s1 := append(s, []int{6, 7, 8}...)
   fmt.Println(s1) // [0 1 2 3 4 5 6 7 8]

   // 删除
   sc := []int{0, 1, 2, 3, 4, 5}
   i := 1
   s2 := append(sc[:i], sc[i+1:]...)
   fmt.Println(s2) // [0 2 3 4 5]

   // 插入
   scc := []int{0, 2, 3, 4, 5}
   s3 := append(scc[:1], append([]int{1}, scc[1:]...)...)
   fmt.Println(s3) // [0 1 2 3 4 5]

   // 反转
   sccc := []int{0, 1, 2, 3, 4, 5}
   for left, right := 0, len(sccc)-1; left < right; left, right = left+1, right-1 {
      sccc[left], sccc[right] = sccc[right], sccc[left]
      //r := s[right]
      //l := s[left]
      //s[left] = r
      //s[right] = l
   }
   fmt.Println(sccc) // [5 4 3 2 1 0]
}

总结

在使用切片时, 我们始终牢记切片本质上就是一个普通的结构体, 里面包含了三个元素「连续内存指针, len, cap」; 出于性能的考虑, 在赋值操作时, 多个切片会共享同一块内存; 当切片触发扩容操作时, 切片指向的连续内存会发生变更。

参考

Go Slices: usage and internals - The Go Programming Language

Arrays, slices (and strings): The mechanics of 'append' - The Go Programming Language

SliceTricks

转载自:https://juejin.cn/post/7208823936300695611
评论
请登录