【Go】P8 Go 语言核心数据结构:深入解析切片 (Slice)

目录

前言

大家好! 在上一篇博文中,我们详细分享了数组(Array)的内容。我们知道,数组是一个拥有相同类型元素固定长度 序列。但在实际开发中,我们更常需要处理可变长度 的序列。今天,我们就来深入探讨 Go 语言中一个极其重要且强大的数据结构------切片(Slice)

切片是 Go 语言的灵魂之一,它提供了比数组更强大、更灵活的序列操作能力。理解切片,是成为一名合格 Go 程序员的必经之路。


什么是切片?

简单来说,切片(Slice)是一个拥有相同类型元素可变长度序列。

它听起来和数组很像,但关键区别在于"可变长度"。切片是对数组的一层封装,它本身并不存储任何数据,而是"引用"一个底层的数组。

切片是一个引用类型,它的内部结构包含了三个核心字段:

  • 指针 (ptr): 指向底层数组中该切片引用的第一个元素。
  • 长度 (len): 切片中实际包含的元素个数。
  • 容量 (cap): 从切片的第一个元素开始,到底层数组末尾的元素个数。

切片的声明与初始化

声明切片的基本语法如下:

go 复制代码
var name []T

其中 T 是切片中元素的类型。

注意: 仅仅声明一个切片时,它的默认值为 nil。一个 nil 切片的长度和容量都是 0,并且没有指向任何底层数组。

go 复制代码
package main

import "fmt"

func main() {
    // 1. 声明一个 nil 切片
    var s1 []int
    fmt.Printf("s1: %v, len: %d, cap: %d, is nil: %t\n", s1, len(s1), cap(s1), s1 == nil)
    // 输出: s1: [], len: 0, cap: 0, is nil: true

    // 2. 使用字面量初始化 (最常用)
    s2 := []int{1, 2, 3, 4}
    fmt.Printf("s2: %v, len: %d, cap: %d, is nil: %t\n", s2, len(s2), cap(s2), s2 == nil)
    // 输出: s2: [1 2 3 4], len: 4, cap: 4, is nil: false

    // 3. 声明一个空切片 (与 nil 切片不同)
    s3 := []int{}
    fmt.Printf("s3: %v, len: %d, cap: %d, is nil: %t\n", s3, len(s3), cap(s3), s3 == nil)
    // 输出: s3: [], len: 0, cap: 0, is nil: false
}

nil 切片和空切片虽然在 lencap 上都为 0,但 nil 切片不指向任何底层内存,而空切片则指向了一个底层的空数组。

基于数组定义切片

切片的核心在于它是数组的"视图"。我们可以通过"切片表达式"从一个数组或另一个切片中创建新切片。

语法:a[low : high]

  • low:起始索引(包含)
  • high:结束索引(不包含)

这将创建一个新切片,其长度为 high - low

go 复制代码
package main

import "fmt"

func main() {
    // 1. 先定义一个数组
    arr := [5]int{10, 20, 30, 40, 50}

    // 2. 基于数组创建切片
    // 取 arr[1] 到 arr[3] (不包括 arr[4])
    s1 := arr[1:4] 

    fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1))
    // 输出: s1: [20 30 40], len: 3, cap: 4

    // 为什么 cap 是 4?
    // 因为切片的容量是从它的第一个元素(arr[1])
    // 一直到底层数组的最后一个元素(arr[4])。
    // 个数是 20(arr[1]), 30(arr[2]), 40(arr[3]), 50(arr[4]),总共 4 个。
}

重要: 切片是引用类型 。修改切片中的元素,会直接修改底层数组中对应的值。

go 复制代码
package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}
    s1 := arr[1:4] // s1 = [20, 30, 40]

    fmt.Println("修改前: arr =", arr) // arr = [10 20 30 40 50]

    // 修改切片 s1 的第一个元素 (对应 arr[1])
    s1[0] = 200

    fmt.Println("修改后: arr =", arr) // arr = [10 200 30 40 50]
    fmt.Println("修改后: s1 =", s1) // s1 = [200 30 40]
}

切片再切片

我们也可以在一个已有的切片上再次进行切片操作,创建子切片。

go 复制代码
package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3, 4, 5, 6}

    // s2 是 s1 的子切片
    s2 := s1[1:3] // [2, 3]
    fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))
    // 输出: s2: [2 3], len: 2, cap: 5  (容量从 s1[1] 到 s1[5] 共 5 个)

    // s3 是 s2 的子切片
    s3 := s2[1:] // [3]  (从 s2[1] 到末尾)
    fmt.Printf("s3: %v, len: %d, cap: %d\n", s3, len(s3), cap(s3))
    // 输出: s3: [3], len: 1, cap: 4 (容量从 s2[1] -> s1[2] 到 s1[5] 共 4 个)

    // 它们都引用同一个底层数组
    s3[0] = 300
    fmt.Println("s1:", s1) // s1: [1 2 300 4 5 6]
    fmt.Println("s2:", s2) // s2: [2 300]
    fmt.Println("s3:", s3) // s3: [300]
}

切片表达式的完整语法: a[low : high : max]

  • low:起始索引(包含)
  • high:结束索引(不包含)
  • max:容量上限(不包含,用于限制新切片的 cap)

新切片的 len = high - lowcap = max - low。这是一种高级用法,可以用来防止子切片通过 append 操作意外地覆盖父切片的数据。

使用 make() 函数构造切片

如果我们不想依赖一个已存在的数组,而是想直接创建一个动态的切片,make() 函数是最好的选择。

make() 函数会分配一个底层的数组,并返回一个指向它的切片。

go 复制代码
// 格式1: make([]T, len)
// 创建一个类型为 T,长度和容量都为 len 的切片
s1 := make([]int, 5) 
// s1: [0 0 0 0 0], len: 5, cap: 5

// 格式2: make([]T, len, cap)
// 创建一个类型为 T,长度为 len,容量为 cap 的切片
s2 := make([]int, 3, 10)
// s2: [0 0 0], len: 3, cap: 10

切片的操作

使用 append() 函数追加元素

append() 是切片最常用的函数,用于向切片末尾追加一个或多个元素。

go 复制代码
package main

import "fmt"

func main() {
    var s []int // s 是一个 nil 切片
    
    // 1. 追加单个元素
    s = append(s, 1) // s = [1]
    
    // 2. 追加多个元素
    s = append(s, 2, 3, 4) // s = [1, 2, 3, 4]
    
    // 3. 追加另一个切片 (注意...语法)
    s2 := []int{5, 6}
    s = append(s, s2...) // s = [1, 2, 3, 4, 5, 6]

    fmt.Println(s)
}

重点: append() 的返回值
append 函数必须 将结果重新赋值给原切片,即 s = append(s, ...)。 为什么?这就涉及到了切片的扩容策略

切片的扩容策略

当我们使用 append 时,如果切片的 len 小于 cap,元素会直接添加到底层数组中,len 增加,切片头结构 (ptr, len, cap) 中的 len 被更新。

但是,当 len 等于 cap 时,切片已满,无法再容纳新元素。此时,Go 的运行时会:

  • 分配一个新数组: 这个新数组的容量会比旧数组大。
  • 复制数据: 将旧数组中的所有元素复制到新数组中。
  • 添加新元素: 在新数组末尾添加要 append 的元素。
  • 返回新切片: append 函数返回一个指向这个新数组的新切片。

这就是为什么我们必须 s = append(s, ...),因为 s 可能已经指向了一个全新的底层数组。

仅需了解:
扩容的规则(大致策略,不同 Go 版本可能微调)

  • 如果切片容量小于 1024(在某些版本是 256),新容量会翻倍(cap * 2)。
  • 如果切片容量大于等于 1024,新容量会增长 1.25 倍(cap * 1.25),以避免内存的过度浪费。
go 复制代码
package main

import "fmt"

func main() {
    s := make([]int, 0, 1) // len=0, cap=1
    
    fmt.Printf("len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)
    
    s = append(s, 1) // len=1, cap=1
    fmt.Printf("len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)
    
    // 此时 cap=1, len=1,再 append 会触发扩容 (cap 翻倍到 2)
    s = append(s, 2) // len=2, cap=2
    fmt.Printf("len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s) // 注意看 ptr 地址变化
    
    // 此时 cap=2, len=2,再 append 会触发扩容 (cap 翻倍到 4)
    s = append(s, 3) // len=3, cap=4
    fmt.Printf("len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s) // ptr 再次变化
}

(注意:%p 打印出的地址在每次运行时都可能不同)

切片的循环遍历

和数组一样,切片主要有两种遍历方式:

go 复制代码
package main

import "fmt"

func main() {
    s := []string{"Apple", "Banana", "Cherry"}

    // 方式一: for 循环 (传统方式)
    fmt.Println("--- for 循环 ---")
    for i := 0; i < len(s); i++ {
        fmt.Printf("Index: %d, Value: %s\n", i, s[i])
    }

    // 方式二: for range 循环 (Go 推荐)
    fmt.Println("--- for range 循环 ---")
    for index, value := range s {
        fmt.Printf("Index: %d, Value: %s\n", index, value)
    }

    // 如果你只关心值 (value)
    fmt.Println("--- for range (仅 value) ---")
    for _, value := range s {
        fmt.Println("Value:", value)
    }
}

切片中的拷贝 (copy 函数)

因为切片是引用类型 ,我们不能简单地用 s2 := s1 来创建一个独立的切片。

  • s2 := s1 这只是浅拷贝(Shallow Copy)s1s2 共享同一个底层数组。修改 s2 会影响 s1

要想创建两个完全独立的切片(互不影响),我们需要使用内置的 copy() 函数进行深拷贝(Deep Copy)

  • copy(dst, src)src 切片中的元素复制到 dst 切片中。
go 复制代码
package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}

    // 陷阱:浅拷贝
    s2 := s1
    s2[0] = 100
    fmt.Println("s1 (浅拷贝后):", s1) // s1: [100 2 3] (s1 被改变了!)
    
    // 正确:深拷贝
    s3 := []int{1, 2, 3}
    // 1. 创建一个和 s3 相同长度的目标切片
    s4 := make([]int, len(s3))
    // 2. 拷贝元素
    copy(s4, s3)
    
    // 3. 修改 s4
    s4[0] = 999
    fmt.Println("s3 (深拷贝后):", s3) // s3: [1 2 3] (s3 未受影响)
    fmt.Println("s4 (深拷贝后):", s4) // s4: [999 2 3]
}

从切片中删除元素

Go 语言中没有提供直接的 "delete" 语法来删除切片中的元素。但我们可以利用 append 的特性来实现删除。

其原理是:将 "被删除元素" 之后的所有元素,拼接到 "被删除元素" 之前的所有元素上

go 复制代码
// 假设要从 s 中删除索引为 index 的元素
s = append(s[:index], s[index+1:]...)
  • s[:index] 获取 0index-1 的所有元素。
  • s[index+1:]... 获取 index+1 到末尾的所有元素(并使用 ... 展开)。
go 复制代码
package main

import "fmt"

func main() {
    s := []string{"A", "B", "C", "D", "E"}

    // 假设我们要删除索引 2 的元素 ("C")
    indexToRemove := 2

    s = append(s[:indexToRemove], s[indexToRemove+1:]...)

    fmt.Println("删除后:", s) // 输出: [A B D E]
}

警告: 这种删除方式会修改底层数组(被删除元素后面的元素会前移)。


数组切片的排序算法

最后,我们来看看如何对切片进行排序。我们介绍三种方法:两种经典的排序算法实现,以及 Go 的标准库 sort

选择排序 (Selection Sort)

逻辑:

  1. 找到切片中最小的元素。
  2. 将它与切片的第一个元素交换位置。
  3. 在剩下的元素中,找到最小的元素。
  4. 将它与切片的第二个元素交换位置。
  5. ...以此类推,直到整个切片排序完成。
go 复制代码
package main

import "fmt"

func selectionSort(items []int) {
    n := len(items)
    for i := 0; i < n; i++ {
        // 假定当前 i 是最小值的索引
        minIndex := i
        // 遍历 i 后面的元素,找到真正的最小值索引
        for j := i + 1; j < n; j++ {
            if items[j] < items[minIndex] {
                minIndex = j
            }
        }
        // 将找到的最小值与 i 位置交换
        items[i], items[minIndex] = items[minIndex], items[i]
    }
}

func main() {
    s := []int{5, 2, 8, 1, 9, 4}
    selectionSort(s)
    fmt.Println("选择排序后:", s) // [1 2 4 5 8 9]
}

冒泡排序 (Bubble Sort)

逻辑:

  1. 比较相邻的两个元素。
  2. 如果第一个比第二个大,就交换它们。
  3. 对切片中的每一对相邻元素做同样的工作,从开始到结束。这会使最大的元素"冒泡"到最后。
  4. 重复以上步骤,但每次比较的范围缩小 1(因为最大的已经归位了)。
  5. ...直到没有元素需要交换。
go 复制代码
package main

import "fmt"

func bubbleSort(items []int) {
    n := len(items)
    for i := 0; i < n; i++ {
        // 标记这一轮是否发生了交换
        swapped := false
        // (n-1-i) 是因为每轮都会把最大的放到最后,所以比较范围缩小
        for j := 0; j < n-1-i; j++ {
            if items[j] > items[j+1] {
                // 交换
                items[j], items[j+1] = items[j+1], items[j]
                swapped = true
            }
        }
        // 如果这一轮没有发生交换,说明已经排好序了
        if !swapped {
            break
        }
    }
}

func main() {
    s := []int{5, 2, 8, 1, 9, 4}
    bubbleSort(s)
    fmt.Println("冒泡排序后:", s) // [1 2 4 5 8 9]
}

sort 包 (Go 语言标准库)

虽然理解冒泡和选择排序很重要,但在实际工作中,我们永远应该使用 Go 标准库 sort 包。它实现了更高效的排序算法(如快速排序)。

sort 包为 []int, []string, []float64 提供了开箱即用的排序方法。

go 复制代码
package main

import (
    "fmt"
    "sort"
)

func main() {
    // 1. 排序 []int
    sInts := []int{5, 2, 8, 1, 9, 4}
    sort.Ints(sInts)
    fmt.Println("sort.Ints:", sInts) // [1 2 4 5 8 9]

    // 2. 排序 []string
    sStrings := []string{"Banana", "Apple", "Cherry"}
    sort.Strings(sStrings)
    fmt.Println("sort.Strings:", sStrings) // [Apple Banana Cherry]

    // 3. 自定义排序 (例如,按长度排序字符串)
    sort.Slice(sStrings, func(i, j int) bool {
        // 定义 "小于" 规则:
        // 如果 sStrings[i] 的长度小于 sStrings[j] 的长度,
        // 那么 sStrings[i] 应该排在前面。
        return len(sStrings[i]) < len(sStrings[j])
    })
    fmt.Println("sort.Slice (by len):", sStrings) // [Apple Cherry Banana]
}

sort.Slice 极其强大,它允许你根据任何自定义逻辑对任意类型的切片进行排序,读者请在实际项目中尝试并理解。


总结

切片是 Go 语言中最灵活、最常用的数据结构。掌握它,你的 Go 编程能力将更上一层楼。

关键点回顾:

  • 切片是引用类型: 它包含 指针、长度 (len) 和 容量 (cap)。
  • 基于数组创建: arr[low:high] 创建的切片与原数组共享内存
  • make() 创建: make([]T, len, cap) 是创建动态切片的标准方式。
  • append() 扩容:len == cap 时,append 会分配新数组,必须 s = append(s, ...)
  • copy() 拷贝: copy(dst, src) 是实现 深拷贝(独立切片) 的唯一途径。
  • sort 包排序: 始终优先使用标准库 sort.Intssort.Slice 进行排序。

希望这篇博文能帮助你彻底理解 Go 语言的切片!在下一篇博文中,我们将探讨 Go 的另一个核心数据结构:map。

感谢阅读!


2025.10.20 金融街

相关推荐
7哥♡ۣۖᝰꫛꫀꪝۣℋ12 小时前
Spring IoC&DI
java·开发语言·mysql
wadesir12 小时前
Go语言反射之结构体的深比较(详解reflect.DeepEqual在结构体比较中的应用)
开发语言·后端·golang
你不是我我13 小时前
【Java 开发日记】我们来说一说 Redis IO 多路复用模型
java·开发语言·redis
想七想八不如1140813 小时前
408操作系统 PV专题
开发语言·算法
浩瀚地学13 小时前
【Java】ArrayList
java·开发语言·经验分享·笔记
阿杰同学13 小时前
Java 设计模式 面试题及答案整理,最新面试题
java·开发语言·设计模式
这样の我13 小时前
java 模拟chrome指纹 处理tls extension顺序
java·开发语言·chrome
yong999013 小时前
基于MATLAB的雷达压制干扰仿真
开发语言·matlab
Genevieve_xiao13 小时前
【数据结构与算法】【xjtuse】面向考纲学习(下)
java·数据结构·学习·算法