首页后端开发GOGo 数组&切片

Go 数组&切片

时间2023-10-21 15:52:03发布访客分类GO浏览1475
导读:数组在开始介绍切片之前需要先介绍一下 go 中的数组。数组是一块连续的存储空间, 定义了存储的类型和长度。下面是是声明长度为 3 的 int 数组, 初始值为 0. 数组可以直接用来比较, 当元素相同时, 返回 ture. 对于数组越界访问...

数组

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

初始化

var arr [3]int            // [0, 0, 0]
arr1 := [...]int{
0, 0, 0}
 // [0, 0, 0]
fmt.Println(arr == arr1)  //比较 true
// 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 函数就可以实现追加操作。

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

初始化

    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, 最后一块空间并没有使用。

s5 := make([]int, 1, 3) // s5[0] =1 s5[]
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))

使用 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]

如果我们期望不使用同一块存储空间, 需要对切片进行单独修改, 可以使用内置的 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 看作一个结构体 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)
}
    

快捷操作

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」; 出于性能的考虑, 在赋值操作时, 多个切片会共享同一块内存; 当切片触发扩容操作时, 切片指向的连续内存会发生变更。

声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!


若转载请注明出处: Go 数组&切片
本文地址: https://pptw.com/jishu/504631.html
Node.js GET、POST 请求是怎样的? Node.js Web 模块的各种用法和常见场景

游客 回复需填写必要信息