欢迎光临
我们一直在努力

【从零开始学习Go语言 | 第八篇】Go语言基础之切片

目录

前言

引子

一、切片是什么?

二、创建切片的方法

1. 通过数组生成切片

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

3. 使用 make 函数创建切片

4. 声明 nil 切片

三、切片的长度和容量

四、切片的动态增长 —— append 函数

五、切片的复制 —— copy 函数

六、切片的遍历

七、切片是引用类型

八、切片的扩容机制

九、切片表达式的高级用法

十、nil 切片和空切片

十一、切片作为函数参数

十二、综合示例:管理学生名单

十三、数组与切片的对比总结

十四、常见陷阱与注意事项

总结


前言

在上一篇文章中,我们学习了数组。数组是固定长度的,一旦声明就无法改变大小。但在实际开发中,我们常常需要动态地增加或删除元素,比如一个在线商城的购物车列表、用户的消息记录等。为了解决这个问题,Go语言提供了切片(Slice)。

切片可以看作是动态数组,它比数组更灵活、更常用。可以说,在Go语言中,切片才是处理序列数据的“主力军”。今天,我们就来深入理解切片:它的底层原理、如何创建和使用,以及它与数组的区别。

引子

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。 例如:

func arraySum(x [3]int) int{
sum := 0
for _, v := range x{
sum = sum + v
}
return sum
}

这个求和函数只能接受[3]int类型,其他的都不支持。 再比如:

a := [3]int{1, 2, 3}

数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素了。

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

一、切片是什么?

切片是对数组的一个连续片段的引用。它本身不存储任何数据,只是底层数组的一个“窗口”。切片的长度可以动态变化,但底层数组的容量是固定的,当切片长度超过容量时,Go会自动分配一个新的更大的底层数组。

切片由三个部分组成:

  • 指针:指向底层数组的起始位置

  • 长度(length):切片中当前元素的个数

  • 容量(capacity):从切片起始位置到底层数组末尾的元素个数

可以这样理解:切片是一个“视图”,让我们能够像操作动态数组一样操作数据。

二、创建切片的方法

1. 通过数组生成切片

可以通过 [low:high] 操作从一个数组(或另一个切片)上切出一段:

arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // 包含索引 1,2,3 → [2,3,4]
fmt.Println(s) // [2 3 4]

切片语法:[start:end],左闭右开,即包含 start 索引,不包含 end 索引。省略 start 表示从0开始,省略 end 表示到末尾。

s1 := arr[:3] // [1,2,3]
s2 := arr[2:] // [3,4,5]
s3 := arr[:] // 整个数组的切片 [1,2,3,4,5]

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

创建切片不需要指定长度(区别于数组需要指定长度或用 …):

// 数组写法
arr := [3]int{1,2,3} // 有长度

// 切片写法
s := []int{1,2,3} // 没有长度,只有方括号
fmt.Printf("%T\\n", s) // []int

字面量方式会在底层创建一个数组,并返回一个引用该数组的切片。

3. 使用 make 函数创建切片

make 是创建切片最灵活的方式,可以指定长度和容量:

s := make([]int, 5) // 长度5,容量也是5,初始值为[0,0,0,0,0]
s2 := make([]int, 3, 5) // 长度3,容量5

make 创建时底层会分配一个数组,所有元素初始化为零值。

4. 声明 nil 切片

var s []int // 未初始化的切片,值为 nil
if s == nil {
fmt.Println("s is nil")
}

nil 切片的长度和容量都是0,但没有底层数组。我们可以直接对 nil 切片使用 append 函数,Go会为其分配底层数组。

三、切片的长度和容量

  • 长度:切片当前包含的元素个数,用 len(s) 获取。

  • 容量:从切片起始位置到底层数组末尾的元素个数,用 cap(s) 获取。

arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4] // [20,30,40]
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 4(从arr[1]到arr[4]共4个元素)

切片的容量决定了在不重新分配底层数组的前提下,切片最多能增长多少。

四、切片的动态增长 —— append 函数

append 是往切片末尾追加元素的内置函数。如果原切片容量足够,它会直接修改底层数组并返回原切片;如果容量不足,Go会分配一个新的底层数组(容量通常翻倍),复制原有元素,然后追加新元素。

s := []int{1, 2, 3}
s = append(s, 4) // [1,2,3,4]
s = append(s, 5, 6) // [1,2,3,4,5,6]

重要:append 返回一个新的切片(可能指向同一个底层数组,也可能是新的),所以必须用原变量接收返回值。

同时可以追加另一个切片,使用 … 解包:

s1 := []int{1,2}
s2 := []int{3,4}
s1 = append(s1, s2…)
fmt.Println(s1) // [1,2,3,4]

五、切片的复制 —— copy 函数

copy 可以将一个切片的数据复制到另一个切片中,复制长度以较小的那个为准。它返回成功复制的元素个数。

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

如果希望完整复制一个独立的切片(深拷贝),需要先 make 一个长度足够的目标切片,然后 copy。

original := []int{1,2,3}
clone := make([]int, len(original))
copy(clone, original)
clone[0] = 100
fmt.Println(original) // [1,2,3] 原切片不受影响

六、切片的遍历

切片的遍历与数组完全一致,可以使用 for 索引或 for range:

s := []string{"Go", "Python", "Java"}
for i, v := range s {
fmt.Printf("索引%d: %s\\n", i, v)
}

// 只需要值
for _, v := range s {
fmt.Println(v)
}

七、切片是引用类型

这一点是切片与数组最大的区别。切片本身是对底层数组的引用。当你将切片赋值给另一个变量或作为函数参数传递时,传递的是引用(其实是一个包含指针、长度、容量的结构体副本,但底层数组指针相同),因此修改切片元素会影响原切片。

func modify(s []int) {
s[0] = 100
}

func main() {
arr := [3]int{1,2,3}
s := arr[:]
modify(s)
fmt.Println(arr) // [100,2,3] 底层数组被修改
}

由于切片是引用类型,传递大切片作为参数非常高效(只拷贝3个机器字),而不会像数组那样拷贝整个元素。

八、切片的扩容机制

当使用 append 导致切片长度超过容量时,Go会分配一个新的底层数组。扩容规则大致是:

  • 如果原切片容量小于1024,新容量会翻倍;

  • 如果原切片容量大于等于1024,新容量会增长1.25倍左右(具体实现可能调整)。

这个策略是为了平衡内存分配和复制开销。我们可以通过代码观察:

s := make([]int, 0, 1) // 初始容量1
oldCap := cap(s)
for i := 0; i < 10; i++ {
s = append(s, i)
if newCap := cap(s); newCap != oldCap {
fmt.Printf("容量变化: %d → %d\\n", oldCap, newCap)
oldCap = newCap
}
}

输出示例:

容量变化: 1 → 2
容量变化: 2 → 4
容量变化: 4 → 8
容量变化: 8 → 16

九、切片表达式的高级用法

除了基本的 [low:high],Go还支持带容量的切片表达式:[low:high:max]。其中 max 用来限制新切片的容量,即容量 = max – low。这样可以将切片的容量限制在底层数组的一部分。

arr := [5]int{0,1,2,3,4}
s := arr[1:3:4] // 长度=2(索引1,2),容量=3(索引1,2,3)
fmt.Println(len(s), cap(s)) // 2 3

十、nil 切片和空切片

  • nil 切片:var s []int,没有底层数组,len=0, cap=0,s == nil 为 true。

  • 空切片:s := []int{} 或 s := make([]int,0),有底层数组(可能是共享的空数组),len=0, cap=0,但 s != nil。

在大多数情况下,函数返回切片时可以返回 nil 切片,因为它可以直接用 append 等操作。判断切片是否为空推荐使用 len(s) == 0,而不是与 nil 比较。

var s1 []int // nil切片
s2 := []int{} // 非nil空切片
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false

十一、切片作为函数参数

因为切片是引用类型,在函数内部修改切片元素会影响外部。但如果要修改切片本身(如通过 append 改变长度或容量),则需要传入切片指针或返回新切片。

func addElement(s []int, val int) []int {
s = append(s, val) // 可能改变底层数组
return s
}

func main() {
s := []int{1,2}
s = addElement(s, 3) // 必须接收返回值
fmt.Println(s) // [1,2,3]
}

十二、综合示例:管理学生名单

下面用切片实现一个简单的学生名单管理,演示动态增加、删除、遍历等功能。

package main

import "fmt"

func main() {
// 初始化学生切片
students := []string{"张三", "李四", "王五"}

// 添加学生
students = append(students, "赵六", "孙七")
fmt.Println("名单:", students)

// 删除索引为2的学生(王五)
index := 2
students = append(students[:index], students[index+1:]…)
fmt.Println("删除后:", students)

// 遍历打印
for i, name := range students {
fmt.Printf("%d. %s\\n", i+1, name)
}
}

输出:

名单: [张三 李四 王五 赵六 孙七]
删除后: [张三 李四 赵六 孙七]
1. 张三
2. 李四
3. 赵六
4. 孙七

十三、数组与切片的对比总结

特性数组切片
长度 固定,类型的一部分 动态,不属于类型
类型表示 [n]T []T
值类型/引用类型 值类型(拷贝整个数组) 引用类型(拷贝指针)
初始化 指定长度或… 不指定长度,或用make
作为函数参数 效率低(大数组拷贝) 效率高(只拷贝描述符)
是否可比较(==) 可比较(同类型) 只能与nil比较
适用场景 固定大小、值语义 动态序列、大多数情况

在实际开发中,99%的情况下使用切片代替数组。数组通常用于底层数据结构或需要值语义的特殊场景。

十四、常见陷阱与注意事项

  • 切片不直接拥有数据:多个切片可以共享同一个底层数组,修改一个会影响另一个。小心“别名”带来的意外。

    s1 := []int{1,2,3,4}
    s2 := s1[1:3] // [2,3]
    s2[0] = 99
    fmt.Println(s1) // [1,99,3,4] 原切片也被修改

  • append 的返回值必须接收:因为底层数组可能重新分配,原切片指向的数组可能失效。

  • 切片的复制是浅拷贝:通过 copy 复制元素是深拷贝,但直接赋值 s2 := s1 只是拷贝了描述符,共享底层数组。

  • 遍历时并发修改不安全:对切片同时读写(例如在 range 中修改切片长度)会有风险,需注意同步。

  • 总结

    今天我们学习了Go语言中最常用的序列类型——切片。

    • 切片是动态数组的抽象,通过指针、长度、容量来引用底层数组。

    • 创建切片的方式:数组切片、字面量、make、nil切片。

    • 常用操作:len、cap、append、copy、切片表达式。

    • 切片是引用类型,传递效率高,但要留意共享底层数组带来的副作用。

    • 使用 append 时一定要接收返回值,因为可能发生内存扩容。

    • 在绝大多数情况下,切片替代数组都是更好的选择。

    掌握了切片,你就掌握了Go语言中数据容器的核心。下一篇文章,我们将继续学习 map(映射),它用于存储键值对,是切片之外的另一个常用数据结构。

    如果你对今天的讲解有任何疑问,或者想进一步探索,欢迎在评论区留言。坚持从零开始,我们一步步掌握Go语言!

    感谢阅读,我们下篇再见!

    如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

    赞(0)
    未经允许不得转载:171主机测评 » 【从零开始学习Go语言 | 第八篇】Go语言基础之切片
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址