Go语言切片:惊人技巧,轻松搞定动态数组!

110 阅读5分钟

Go语言的切片(slice)是一个灵活且强大的数据结构。相比数组,切片的长度可以动态变化,更适合用于处理动态数据。切片是基于数组构建的抽象,为开发者提供了更高效的内存管理和数据操作手段。

一、切片的概念和结构

切片其实是对底层数组的一个引用。它包含三个核心信息:

  1. 指向底层数组的指针(Pointer) - 指向切片的第一个元素(在底层数组中的位置)。
  2. 切片的长度(Length) - 当前切片中实际包含的元素数。
  3. 切片的容量(Capacity) - 从切片的起始位置到底层数组末尾的元素总数。

切片在Go语言中被设计为动态扩展的数据结构,通过共享底层数组实现快速操作和高效的内存管理。

二、切片的创建方式

切片有多种创建方式,具体如下:

1. 从数组创建切片

arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4]  // 从arr数组的第1到第3个元素创建一个切片
fmt.Println(s) // 输出:[20 30 40]

在上例中,arr[1:4]的含义是从数组的索引1(包含)到索引4(不包含)生成一个切片。切片len(s)为3,cap(s)为4。

2. 通过make函数创建切片

make函数可以创建一个指定长度和容量的切片。其格式为:make([]Type, length, capacity)

s := make([]int, 3, 5) // 创建一个长度为3,容量为5的切片
fmt.Println(s)         // 输出:[0 0 0]
fmt.Println(len(s))    // 输出:3
fmt.Println(cap(s))    // 输出:5

如果省略容量参数,则容量和长度相同。

3. 直接使用字面量创建切片

s := []int{1, 2, 3} // 创建一个包含元素1, 2, 3的切片,长度和容量都为3
fmt.Println(s)      // 输出:[1 2 3]

三、切片的属性

通过内置函数lencap可以分别获取切片的长度和容量。

s := []int{1, 2, 3, 4, 5}
fmt.Println("长度:", len(s)) // 输出:5
fmt.Println("容量:", cap(s)) // 输出:5

当我们基于一个切片或数组进行“切片操作”时,容量会从新的切片起始位置算起,到底层数组的末尾。

四、切片的常用操作

1. 切片的追加操作(append)

append是一个用于向切片追加元素的内置函数。若切片容量不足时,append会创建一个新的底层数组,将旧数据复制过去,然后再追加新元素。

s := []int{1, 2, 3}
s = append(s, 4, 5)   // 追加元素4, 5
fmt.Println(s)        // 输出:[1 2 3 4 5]
fmt.Println(len(s))   // 输出:5
fmt.Println(cap(s))   // 如果之前容量为3,现在可能变成6

2. 切片的切片(子切片)

切片也可以再次“切片”,生成一个新的子切片。子切片共享原切片的底层数组,因此对数据的更改会影响彼此。

s := []int{1, 2, 3, 4, 5}
sub := s[1:4]  // 创建从索引1到索引3的子切片
fmt.Println(sub)  // 输出:[2 3 4]

sub[0] = 20      // 修改sub的第一个元素
fmt.Println(s)   // 输出:[1 20 3 4 5],s也被修改

五、切片的扩容机制

切片的底层数组容量是有限的,如果超出容量,Go语言会自动扩容。扩容时,Go会创建一个新的、较大的底层数组,并将旧数组的数据拷贝到新数组中。扩容的策略通常是成倍扩展容量,但并不保证每次都是相同倍数。

例如,当容量不够时,以下代码展示了扩容的现象:

s := make([]int, 2, 2)
s[0] = 1
s[1] = 2
fmt.Println(len(s), cap(s))  // 输出:2 2

s = append(s, 3)            // 追加一个元素
fmt.Println(len(s), cap(s))  // 输出:3 4,容量翻倍

在上例中,append使切片的容量从2变为4。

六、切片的拷贝(copy函数)

copy函数用于将一个切片的内容复制到另一个切片中,格式为:copy(dst, src)。它会根据目标切片的长度,复制最多与dst长度相同的元素数。

src := []int{1, 2, 3}
dst := make([]int, 2)
copy(dst, src)  // 只会复制src的前两个元素到dst中
fmt.Println(dst)  // 输出:[1 2]

七、切片是引用类型的注意事项

切片是引用类型,因此多个切片可能共享同一个底层数组。这种情况下,一个切片的修改可能会影响另一个切片的内容。为了避免这种问题,可以在切片上创建一个副本。

original := []int{1, 2, 3}
copySlice := make([]int, len(original))
copy(copySlice, original) // 将original内容复制到copySlice
copySlice[0] = 100
fmt.Println(original)  // 输出:[1 2 3]
fmt.Println(copySlice) // 输出:[100 2 3]

八、示例代码总结

以下代码展示了切片的创建、追加、切片操作和拷贝:

package main
import "fmt"

func main() {
    // 基于数组创建切片
    arr := [5]int{1, 2, 3, 4, 5}
    s := arr[1:4]
    fmt.Println("切片内容:", s)
    fmt.Println("切片长度:", len(s))
    fmt.Println("切片容量:", cap(s))

    // 追加元素
    s = append(s, 6)
    fmt.Println("追加后切片:", s)

    // 创建切片副本
    copySlice := make([]int, len(s))
    copy(copySlice, s)
    fmt.Println("副本切片:", copySlice)
    
    // 子切片修改影响原切片
    subSlice := s[1:3]
    subSlice[0] = 100
    fmt.Println("修改子切片后,原切片:", s)
}

以上代码展示了创建切片、追加元素、复制切片,以及子切片修改对原切片的影响等操作。