Golang中的切片,一篇文章带你深度理解切片!

Golang 中的数组与 slice

数组

在Go语言中,数组是一种固定长度且元素类型一致的序列。数组的长度在定义时就已确定,并且在数组的生命周期中无法改变。

数组的特性:

  1. 固定长度: 数组的长度在定义时就已确定,无法在运行时改变。
  2. 零值初始化: 未显式初始化的数组元素会被自动赋予类型的零值(例如,整数类型的零值为0,字符串类型的零值为空字符串,指针类型的零值为nil)。
  3. 值语义: 当数组被赋值给另一个数组或作为参数传递给函数时,实际是对整个数组进行拷贝,而不是引用传递。
  4. 多维数组: Go支持多维数组。

创建数组的方式:

  1. 通过字面量创建:
 var arr1 [5]int              // 声明一个长度为5的整数数组,默认值为0
 arr2 := [3]string{"a", "b", "c"} // 声明并初始化一个长度为3的字符串数组
  1. 通过指定长度和初始值创建:
 arr := [...]int{1, 2, 3, 4, 5} // 编译器会根据初始值的个数推断数组的长度
  1. 多维数组:
 var matrix [3][3]int // 声明一个3x3的二维整数数组

Slice

slice(切片)是一种动态数组,它提供了一种方便且高效的方式来操作序列化的数据集合。Slice 是对数组的一个抽象,它通过指向数组的指针、长度和容量来描述数组的一段连续片段。

切片的底层结构

切片在Go语言的底层是由三个字段组成的结构体:

  1. 指针(Pointer):指向底层数组的第一个元素。
  2. 长度(Length):切片中的元素个数。
  3. 容量(Capacity):从切片的第一个元素开始到底层数组的最后一个元素的个数。
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

Slice的特性:

  1. 动态大小: Slice可以动态增长或缩减,而无需复制整个数组。
  2. 引用语义: 当传递slice给函数或方法时,实际上传递的是指向底层数组的引用,因此对slice的修改会影响底层数组的对应部分。
  3. 零值: slice的零值为nil,表示未指向任何数组。
  4. 长度和容量:
    • 长度(length): 表示slice当前包含的元素个数,可以通过内置函数len()获取。
    • 容量(capacity): 表示slice开始位置到底层数组末尾的元素个数,可以通过内置函数cap()获取。

创建slice的方式:

  1. 使用make函数: make([]T, length, capacity),其中T是slice中元素的类型,length是初始长度,capacity是预分配的容量。
 slice := make([]int, 5, 10) // 创建一个初始长度为5,容量为10的int类型slice
  1. 通过字面量: 直接通过数组或其他slice来创建。
 arr := [5]int{1, 2, 3, 4, 5}
 slice := arr[1:4] // 创建一个从数组arr索引1到索引3的slice,包含元素{2, 3, 4}

操作slice:

  • 访问元素: 可以像数组一样使用索引访问slice中的元素。
  • 追加元素: 使用内置函数append()向slice中追加元素。如果slice的容量不足,append()函数会自动扩展容量。
   slice := []int{1, 2, 3}
   slice = append(slice, 4) // 追加元素4到slice中
  • 复制slice: 使用内置函数copy()复制一个slice到另一个slice。
   slice1 := []int{1, 2, 3}
   slice2 := make([]int, len(slice1))
   copy(slice2, slice1) // 复制slice1到slice2

数组与切片的区别

特性数组切片
长度固定,定义时确定,不能改变动态,可增长或缩减
零值数组类型的零值是包含零值的数组切片类型的零值是nil
内存分配内存一次性分配内存按需分配
值/引用值类型,赋值或传参时会拷贝整个数组引用类型,赋值或传参时只拷贝引用
创建方式var arr [5]intmake([]int, len, cap)[]int{}
使用场景用于需要固定大小的集合用于需要动态大小的集合

示例代码:

package main

import "fmt"

func main() {
    // 数组示例
    var arr [3]int = [3]int{1, 2, 3}
    fmt.Println("数组:", arr)

    // 修改数组元素
    arr[1] = 20
    fmt.Println("修改后的数组:", arr)

    // 切片示例
    slice := []int{1, 2, 3}
    fmt.Println("切片:", slice)

    // 向切片追加元素
    slice = append(slice, 4)
    fmt.Println("追加元素后的切片:", slice)

    // 切片切割
    newSlice := slice[1:3]
    fmt.Println("切片切割:", newSlice)
}

Slice 的扩容

Go语言中的切片具有动态扩容的特性,当向切片追加元素且容量不足时,切片会自动扩容。切片的扩容机制是通过内置函数append实现的。理解切片的扩容原理有助于编写高效的Go代码。

扩容的过程

当使用append函数向切片追加元素时,如果切片的容量不足以容纳新元素,Go会自动执行以下步骤来扩容:

  1. 分配新的底层数组:根据当前切片的容量和即将追加的元素数量,计算新的容量,并分配一个新的底层数组。
  2. 复制数据:将旧的底层数组中的数据复制到新的底层数组中。
  3. 更新指针和容量:将切片的指针指向新的底层数组,并更新切片的容量。

容量增长策略

Go语言采用了一种几何增长策略来扩容切片,以确保扩容操作的效率。具体策略如下:

  • 当切片的当前容量小于1024时,新的容量将是旧容量的两倍。
  • 当切片的当前容量大于或等于1024时,新的容量将增加当前容量的1/4。

以下是一个示例,展示了切片在追加元素时的扩容过程:

package main

import "fmt"

func main() {
    slice := make([]int, 0, 2)
    fmt.Printf("初始容量: %d\n", cap(slice))

    // 追加元素,触发扩容
    for i := 0; i < 10; i++ {
        slice = append(slice, i)
        fmt.Printf("追加元素 %d 后的长度: %d, 容量: %d\n", i, len(slice), cap(slice))
    }
}

示例输出

初始容量: 2
追加元素 0 后的长度: 1, 容量: 2
追加元素 1 后的长度: 2, 容量: 2
追加元素 2 后的长度: 3, 容量: 4
追加元素 3 后的长度: 4, 容量: 4
追加元素 4 后的长度: 5, 容量: 8
追加元素 5 后的长度: 6, 容量: 8
追加元素 6 后的长度: 7, 容量: 8
追加元素 7 后的长度: 8, 容量: 8
追加元素 8 后的长度: 9, 容量: 16
追加元素 9 后的长度: 10, 容量: 16

注意事项

  1. 性能影响:频繁的扩容操作会带来性能开销,因为每次扩容都涉及到内存分配和数据复制。因此,如果可以预估切片的最终大小,最好在创建切片时直接分配足够的容量。
  2. 内存浪费:由于切片的容量通常是按倍数增加的,有时会分配多余的内存空间,导致一定的内存浪费。

内存共享问题

切片中的内存共享机制意味着多个切片可以引用同一个底层数组的不同部分,从而实现高效的数据操作和传递。

内存共享特性

  1. 共享底层数组:多个切片可以共享同一个底层数组的不同部分。当一个切片的元素发生改变时,其他引用同一个底层数组的切片也会受到影响。
  2. 引用传递:切片是引用类型,当切片被传递给函数或被赋值给另一个切片时,实际上传递的是底层数组的引用,而不是整个数组的拷贝。

示例代码

package main

import "fmt"

func main() {
    // 创建一个底层数组
    arr := [5]int{1, 2, 3, 4, 5}

    // 创建两个切片,分别引用底层数组的不同部分
    slice1 := arr[0:3]
    slice2 := arr[2:5]

    fmt.Println("初始状态:")
    fmt.Println("arr:", arr)
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)

    // 修改slice1的元素
    slice1[2] = 100

    fmt.Println("\n修改slice1[2]后的状态:")
    fmt.Println("arr:", arr)
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)
}

示例输出

初始状态:
arr: [1 2 3 4 5]
slice1: [1 2 3]
slice2: [3 4 5]

修改slice1[2]后的状态:
arr: [1 2 100 4 5]
slice1: [1 2 100]
slice2: [100 4 5]

在这个示例中,slice1slice2共享同一个底层数组arr。修改slice1中的元素会影响到slice2,因为它们都引用了相同的底层数组。

注意事项

  1. 数据一致性:由于切片共享底层数组,修改一个切片中的数据会影响到其他引用同一底层数组的切片。这在某些情况下可能会引发数据一致性问题,需要特别注意。
  2. 避免误用:在进行切片操作时,特别是在传递切片给函数或在不同部分间共享切片时,要小心处理,避免意外修改共享数据。
  3. 容量限制:切片的容量是从切片的起始位置到底层数组末尾的元素个数。如果切片操作超过其容量范围,可能会导致运行时错误或意外的行为。

深拷贝与浅拷贝

  • 浅拷贝:切片赋值或传递时是浅拷贝,只复制切片结构体中的指针、长度和容量,底层数组不复制。
  • 深拷贝:若需要独立的副本,可以通过内置函数copy进行深拷贝,创建一个新的底层数组并复制数据。
package main

import "fmt"

func main() {
    // 创建一个底层数组
    arr := [5]int{1, 2, 3, 4, 5}
    slice1 := arr[:]

    // 深拷贝
    slice2 := make([]int, len(slice1))
    copy(slice2, slice1)

    fmt.Println("初始状态:")
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)

    // 修改slice1的元素
    slice1[2] = 100

    fmt.Println("\n修改slice1[2]后的状态:")
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)
}

示例输出

初始状态:
slice1: [1 2 3 4 5]
slice2: [1 2 3 4 5]

修改slice1[2]后的状态:
slice1: [1 2 100 4 5]
slice2: [1 2 3 4 5]

在这个示例中,slice2slice1的深拷贝,修改slice1的元素不会影响slice2

子切片与原切片

子切片与原始切片之间的内存共享机制是一个重要特性。子切片(subslices)是从原始切片(parent slice)中截取出来的一部分,它们共享相同的底层数组。

子切片的创建

子切片是通过切片操作符 slice[low:high] 从原始切片中创建的,其中 lowhigh 是索引,表示子切片的起始位置(包含)和结束位置(不包含)。

内存共享机制

  1. 共享底层数组:子切片和原始切片共享同一个底层数组。这意味着对子切片的修改可能会影响原始切片,反之亦然,因为它们都指向相同的底层数据。
  2. 长度和容量:子切片的长度是 high - low,容量是从 low 开始到底层数组末尾的元素个数。

示例代码

package main

import "fmt"

func main() {
    // 创建一个原始切片
    original := []int{1, 2, 3, 4, 5}

    // 创建子切片
    subSlice := original[1:4]

    fmt.Println("初始状态:")
    fmt.Println("original:", original)
    fmt.Println("subSlice:", subSlice)

    // 修改子切片中的元素
    subSlice[1] = 100

    fmt.Println("\n修改subSlice[1]后的状态:")
    fmt.Println("original:", original)
    fmt.Println("subSlice:", subSlice)
}

示例输出

初始状态:
original: [1 2 3 4 5]
subSlice: [2 3 4]

修改subSlice[1]后的状态:
original: [1 2 100 4 5]
subSlice: [2 100 4]

在这个示例中,subSlice 是从 original 切片中创建的。修改 subSlice 的元素会影响 original,因为它们共享同一个底层数组。

注意事项

  1. 数据一致性:由于内存共享,修改子切片中的数据会影响原始切片,这在某些情况下可能会引发数据一致性问题。需要特别小心在并发环境下的切片操作,以避免竞态条件。
  2. 容量限制:子切片的容量是从 low 开始到底层数组末尾的元素个数。如果子切片的操作超过其容量范围,可能会导致运行时错误或未定义行为。

扩展子切片的影响

当使用 append 函数向子切片添加元素时,如果添加的元素使得子切片的容量不足,Go会分配一个新的底层数组,并将现有元素复制到新的数组中。这时,原始切片和子切片将不再共享同一个底层数组。

package main

import "fmt"

func main() {
    // 创建一个原始切片
    original := []int{1, 2, 3, 4, 5}

    // 创建子切片
    subSlice := original[1:4]

    // 向子切片追加元素,触发扩容
    subSlice = append(subSlice, 6, 7, 8)

    fmt.Println("追加元素后的状态:")
    fmt.Println("original:", original)
    fmt.Println("subSlice:", subSlice)
}

示例输出

追加元素后的状态:
original: [1 2 3 4 5]
subSlice: [2 3 4 6 7 8]

在这个示例中,向 subSlice 追加元素导致了切片的扩容,新的元素被添加到一个新的底层数组中,subSliceoriginal 切片之间的共享关系被打破。

深拷贝子切片

如果需要创建一个独立的子切片,而不希望它与原始切片共享内存,可以使用 copy 函数来实现深拷贝。

package main

import "fmt"

func main() {
    // 创建一个原始切片
    original := []int{1, 2, 3, 4, 5}

    // 创建子切片
    subSlice := original[1:4]

    // 创建一个独立的副本
    independentSlice := make([]int, len(subSlice))
    copy(independentSlice, subSlice)

    fmt.Println("初始状态:")
    fmt.Println("original:", original)
    fmt.Println("subSlice:", subSlice)
    fmt.Println("independentSlice:", independentSlice)

    // 修改子切片中的元素
    subSlice[1] = 100

    fmt.Println("\n修改subSlice[1]后的状态:")
    fmt.Println("original:", original)
    fmt.Println("subSlice:", subSlice)
    fmt.Println("independentSlice:", independentSlice)
}

示例输出

初始状态:
original: [1 2 3 4 5]
subSlice: [2 3 4]
independentSlice: [2 3 4]

修改subSlice[1]后的状态:
original: [1 2 100 4 5]
subSlice: [2 100 4]
independentSlice: [2 3 4]

在这个示例中,independentSlicesubSlice 的深拷贝,修改 subSlice 的元素不会影响 independentSlice

总结

本文深度对比了golang中的数组和切片,重点介绍了切片的常见操作与常见的误区。要非常注意切片的扩容策略,内存共享问题。这些都是面试中的要点。

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值