Go语言切片:动态灵活的数据序列

文章目录

  • Go语言切片:动态灵活的数据序列
    • 一、引言
    • 二、切片是什么
    • 三、切片的结构
    • 四、切片的创建
      • [(一)、创建切片的标准方式:make 函数](#(一)、创建切片的标准方式:make 函数)
        • [1. make 函数完整语法](#1. make 函数完整语法)
        • [2、make 创建切片的 3个核心注意点(必看)](#2、make 创建切片的 3个核心注意点(必看))
          • [(1). 容量不能小于长度](#(1). 容量不能小于长度)
          • [(2). make 创建的切片会**默认初始化**](#(2). make 创建的切片会默认初始化)
          • [(3). 预留容量,避免频繁扩容(性能关键)](#(3). 预留容量,避免频繁扩容(性能关键))
        • [3. 2种常用创建示例](#3. 2种常用创建示例)
      • (二)直接声明切片字面量
      • [(三)字面量声明 vs make 函数创建(核心对比)](#(三)字面量声明 vs make 函数创建(核心对比))
        • [1. 切片字面量 `[]int{10,20,30}`](#1. 切片字面量 []int{10,20,30})
        • [2. make 函数 `make([]int, 3, 5)`](#2. make 函数 make([]int, 3, 5))
    • [五、空切片(nil 切片)](#五、空切片(nil 切片))
      • [(一)定义与核心特性](#(一)定义与核心特性)
      • [(二)、nil 切片 VS make([]int, 0) 深度对比](#(二)、nil 切片 VS make([]int, 0) 深度对比)
      • [(三)、make 创建切片的 4 种方式对比(零基础必看)](#(三)、make 创建切片的 4 种方式对比(零基础必看))
        • [① var s []int → nil 切片(空切片)](#① var s []int → nil 切片(空切片))
        • [② s1 := make([]int, 0) → 零长度切片](#② s1 := make([]int, 0) → 零长度切片)
        • [③ s2 := make([]int, 10) → 长度=容量](#③ s2 := make([]int, 10) → 长度=容量)
        • [④ s3 := make([]int, 0, 10) → 预分配容量](#④ s3 := make([]int, 0, 10) → 预分配容量)
        • [🔥 完整代码示例(4 种切片一次性演示)](#🔥 完整代码示例(4 种切片一次性演示))
        • [✅ 运行结果(真实输出)](#✅ 运行结果(真实输出))
        • [🔍 结果极简说明](#🔍 结果极简说明)
    • 六、切片的操作
    • 七、切片与数组的关系
    • 八、切片的注意事项
    • 九、总结
  • [技术标签:#Go语言 #切片 #数据结构 #编程入门 #动态数组 #nil切片](#Go语言 #切片 #数据结构 #编程入门 #动态数组 #nil切片)

Go语言切片:动态灵活的数据序列

一、引言

大家好!在上一篇博客中,我们深入了解了 Go 语言数组,知晓它如同固定格子数量的书架,具有长度固定等特性。今天,我们将走进与数组紧密相关的切片的世界。切片就像是一个可以根据需求灵活调整大小的书架,为我们在编程中处理数据带来了极大的便利。在深入了解切片的各种操作之前,先让我们认识一下切片的内部结构,这将有助于我们更好地理解切片的行为和特性。

二、切片是什么

切片是 Go 语言中灵活且动态的数据序列,它基于数组实现,但克服了数组长度固定、无法扩展的缺点。

你可以把切片理解成:
底层数组的一个"动态窗口"------通过这个窗口,你可以观察、修改数组的一部分或全部数据,并且窗口大小可以随时变化。

切片自身不真正存储数据,数据都存放在底层数组中,切片只是对数组的引用

在 Go 运行时中,切片本质是一个固定的结构体,包含3个核心字段

go 复制代码
// 切片的真实底层结构(runtime 内部定义)
type slice struct {
    array unsafe.Pointer // 1. 指针:指向底层数组的起始元素
    len   int            // 2. 长度:当前可访问的元素个数
    cap   int            // 3. 容量:底层数组从指针开始的最大可用长度
}

三、切片的结构

切片在 Go 运行时内部,是一个固定的结构体,包含三个关键字段:

  • 指向底层数组的指针:记录切片从数组的哪个位置开始
  • 长度(len) :切片当前实际拥有的元素个数
  • 容量(cap):从指针位置到数组末尾,一共还能存放多少元素(不扩容前提下)

切片结构可视化

复制代码
+---------------------------+
|      切片结构体 Slice       |
+---------------------------+
| 指针 *int → 指向数组[0]      |
| 长度  len = 3              |
| 容量  cap = 5              |
+---------------------------+
           │
           ▼
+-------------------------------+
|        底层数组 [5]int         |
+-------------------------------+
|  0  |  1  |  2  |  3  |  4    |
+-------------------------------+
  └─────────┘       └──────────┘
    len=3(可用)       剩余空间

简单总结:

  • len:你现在能用多少
  • cap:你最多还能用多少(不扩容)

四、切片的创建

(一)、创建切片的标准方式:make 函数

开发中,make 是创建切片最推荐、最常用的方式,它会自动分配底层数组。

1. make 函数完整语法
复制代码
// 语法
make([]T, len, cap)

// 参数说明
// T:切片存储的元素类型(int/string/struct 等)
// len:切片长度(必填)
// cap:切片容量(可选,省略则默认等于长度)

回到开头的图解:对应完整代码

我们把结构图变成可运行的 Go 代码,验证结构一致性:

go 复制代码
package main

import "fmt"

func main() {
	// 1. 底层数组:5个元素 [0,1,2,3,4]
	arr := [5]int{0, 1, 2, 3, 4}

	// 2. 创建切片:指向数组首元素,len=3,cap=5
	s := arr[:3]

	// 3. 输出验证
	fmt.Println("切片值:", s)
	fmt.Println("长度 len:", len(s))
	fmt.Println("容量 cap:", cap(s))
}

运行结果

复制代码
切片值: [0 1 2]
长度 len: 3
容量 cap: 5

和我们的结构图完全一致


2、make 创建切片的 3个核心注意点(必看)
(1). 容量不能小于长度

这是最常见的错误,编译直接报错:

go 复制代码
// 错误:cap 不能 < len
s := make([]int, 5, 3)

规则容量 ≥ 长度 永远成立。

(2). make 创建的切片会默认初始化

make 会分配内存并将元素清零(int=0,string="",bool=false),不会出现野值。

(3). 预留容量,避免频繁扩容(性能关键)

如果预先知道数据量,指定容量 > 长度,可以避免切片自动扩容:

go 复制代码
// 推荐:提前预留足够容量,性能更高
userList := make([]User, 0, 100)
3. 2种常用创建示例
go 复制代码
package main

import "fmt"

func main() {
	// 1. 只传长度:容量 = 长度
	s1 := make([]int, 3)
	fmt.Println("s1:", s1, "len:", len(s1), "cap:", cap(s1))

	// 2. 传长度+容量:预留空间,提升性能
	s2 := make([]int, 3, 5)
	fmt.Println("s2:", s2, "len:", len(s2), "cap:", cap(s2))

	
}

运行结果

复制代码
s1: [0 0 0] len: 3 cap: 3
s2: [0 0 0] len: 3 cap: 5

我给你大幅扩充、丰富、专业、零基础友好 地重写这一段,多角度举例 + 和 make 清晰对比,直接替换你原文即可,博客瞬间更饱满、更专业!


(二)直接声明切片字面量

除了使用 make 函数,我们还可以直接声明并初始化切片字面量 ,这种方式和声明数组有些相似,但不需要指定长度,Go 会自动推断为切片。

它的特点:

  • 写法直观、简洁
  • 自动分配底层数组
  • 自动设置 len = cap = 元素个数
  • 适合已知具体元素的场景
1. 基础示例
go 复制代码
package main

import "fmt"

func main() {
    // 直接声明并初始化切片
    s := []int{10, 20, 30}
    
    fmt.Println("切片:", s)
    fmt.Println("长度len:", len(s))
    fmt.Println("容量cap:", cap(s))
}

运行结果:

复制代码
切片: [10 20 30]
长度len: 3
容量cap: 3

2. 多角度举例
示例1:字符串类型切片
复制代码
s := []string{"Go", "Java", "Python"}
示例2:布尔类型切片
复制代码
s := []bool{true, false, true}
示例3:空元素切片(长度为0)
复制代码
s := []int{}

⚠️ 注意:这是零长度切片,不是 nil 切片。

示例4:只声明不初始化(nil 切片)
go 复制代码
var s []int

(三)字面量声明 vs make 函数创建(核心对比)

这是初学者必须掌握的知识点,两种方式适用场景完全不同:

1. 切片字面量 []int{10,20,30}
  • 适合:已经知道具体元素值
  • 自动设置:len = cap = 元素数量
  • 无需手动管理长度和容量
  • 代码更短、更直观
2. make 函数 make([]int, 3, 5)
  • 适合:暂时不知道元素值,但需要预先分配空间
  • 可手动指定 lencap
  • 元素自动初始化为零值(0、false、"")
  • 适合后续大量 append 操作,性能更高

创建方式 适用场景 len/cap 元素初始值
[]int{10,20,30} 已知具体元素 len=cap=3 手动赋值
make([]int, 3) 预分配长度,后续填充 len=cap=3 自动零值
make([]int, 0, 10) 预分配容量,后续append len=0, cap=10 无元素

总结

  • 切片字面量:简单直观,适合已知元素的场景;
  • make:灵活可控,适合预先分配内存、追求性能的场景;
  • 两者最终都会创建切片并分配底层数组,只是使用姿势不同

五、空切片(nil 切片)

(一)定义与核心特性

Go 官方定义:未初始化的切片变量就是空切片,也叫 nil 切片

声明方式:

go 复制代码
var nilSlice []int

为什么它叫空切片?

核心原因:切片内部的指针字段是空指针(nil),没有指向任何底层数组内存地址。

空切片三大特征:

  1. 内部指针:为空 nil,不指向任何底层数组
  2. 长度 len = 0,容量 cap = 0
  3. 没有分配任何底层数组内存,只是一个切片结构体零值

代码示例 + 打印地址

go 复制代码
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var nilSlice []int

	// 打印切片信息、是否为nil、底层数组地址
	fmt.Printf("空切片(nil切片):%v\n", nilSlice)
	fmt.Printf("len=%d  cap=%d\n", len(nilSlice), cap(nilSlice))
	fmt.Printf("是否为nil:%t\n", nilSlice == nil)
	// 强制打印底层数组指针地址
	fmt.Printf("底层数组指针地址:%#x\n", *(*uintptr)(unsafe.Pointer(&nilSlice)))
}

运行结果:

复制代码
空切片(nil切片):[]
len=0  cap=0
是否为nil:true
底层数组指针地址:0x0

解读 :地址为 0x0 代表指针是空地址,没有绑定任何底层数组,这就是空切片名字的由来

空切片内存结构示意图

复制代码
+----------------------+
|    空切片 结构体      |
+----------------------+
| 指针:nil(地址0x0)    |
| len:0               |
| cap:0               |
+----------------------+
          无
     无底层数组

(二)、nil 切片 VS make([]int, 0) 深度对比

很多初学者迷惑:

两个打印出来都是 [],len 和 cap 也都是 0,到底有什么区别?

1. 核心本质区别
  • var s []intnil 空切片
    指针 = nil,地址为 0,无底层数组
  • s1 := make([]int, 0)非空零长度切片
    指针 有真实内存地址已分配底层数组,只是没有存放元素
2. 完整对比代码(打印地址+nil判断)
go 复制代码
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// 1. nil 空切片
	var nilSlice []int
	fmt.Println("========== nil 空切片 ==========")
	fmt.Printf("值:%v\n", nilSlice)
	fmt.Printf("len=%d cap=%d\n", len(nilSlice), cap(nilSlice))
	fmt.Printf("是否nil:%t\n", nilSlice == nil)
	fmt.Printf("底层数组指针地址:%#x\n\n", *(*uintptr)(unsafe.Pointer(&nilSlice)))

	// 2. make([]int, 0) 创建的切片
	emptySlice := make([]int, 0)
	fmt.Println("====== make([]int, 0) 切片 ======")
	fmt.Printf("值:%v\n", emptySlice)
	fmt.Printf("len=%d cap=%d\n", len(emptySlice), cap(emptySlice))
	fmt.Printf("是否nil:%t\n", emptySlice == nil)
	fmt.Printf("底层数组指针地址:%#x\n", *(*uintptr)(unsafe.Pointer(&emptySlice)))
}
运行结果示例
复制代码
========== nil 空切片 ==========
值:[]
len=0 cap=0
是否nil:true
底层数组指针地址:0x0

====== make([]int, 0) 切片 ======
值:[]
len=0 cap=0
是否nil:false
底层数组指针地址:0x1400001a080
结果通俗解读
  1. nil 空切片:指针地址是 0x0空指针,没分配数组;
  2. make([]int,0):指针是正常内存地址,已经分配了底层数组,只是长度为0、没放元素;
  3. 只有 var s []int 是真正 nil,make 创建的切片永远不等于 nil
内存结构对比图
复制代码
【nil 空切片】
+----------------------+
| 指针:nil(0x0)       |
| len:0               |
| cap:0               |
+----------------------+
        无底层数组


【make([]int, 0) 切片】
+----------------------+
| 指针:0x1400001a080 |
| len:0               |
| cap:0               |
+----------------------+
           │
           ▼
    底层空数组内存
(已分配,只是无元素)

小结

  1. 空切片叫空切片,根本原因是内部指针为 nil 空地址
  2. var s []int 是 nil 空切片,无底层数组;
  3. make([]int,0) 不是 nil,有底层数组,只是长度为0;
  4. 两者肉眼打印一样,底层内存结构完全不同,可通过指针地址精准区分。

(三)、make 创建切片的 4 种方式对比(零基础必看)

为了让你彻底理解 lencap,我们一次性对比 4 种最常见的切片创建方式,帮你一次性理清所有疑惑。

① var s []int → nil 切片(空切片)
  • 指针:nil
  • len:0
  • cap:0
  • 无底层数组
② s1 := make([]int, 0) → 零长度切片
  • 指针:有真实地址
  • len:0
  • cap:0
  • 已分配空数组
③ s2 := make([]int, 10) → 长度=容量
  • len:10
  • cap:10
  • 元素自动初始化为 0
  • 适合:已知元素数量
④ s3 := make([]int, 0, 10) → 预分配容量
  • len:0
  • cap:10
  • 已分配数组,预留空间
  • 性能最优,推荐使用

超级总结表(一眼看懂区别)

复制代码
切片写法                | 指针    | len | cap | 底层数组
------------------------|---------|-----|-----|----------------
var s []int             | nil     | 0   | 0   | 无
s1 := make([]int, 0)    | 有地址  | 0   | 0   | 已分配空数组
s2 := make([]int, 10)   | 有地址  | 10  | 10  | 已分配长度10数组
s3 := make([]int, 0, 10)| 有地址 | 0   | 10  | 已分配容量10数组

🔥 完整代码示例(4 种切片一次性演示)
go 复制代码
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	fmt.Println("========================")
	fmt.Println("① var s []int → nil切片")
	var s []int
	fmt.Printf("切片: %v\n", s)
	fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
	fmt.Printf("是否为nil: %t\n", s == nil)
	fmt.Printf("底层指针地址: %#x\n\n", *(*uintptr)(unsafe.Pointer(&s)))

	fmt.Println("========================")
	fmt.Println("② s1 := make([]int, 0) → 零长度切片")
	s1 := make([]int, 0)
	fmt.Printf("切片: %v\n", s1)
	fmt.Printf("len: %d, cap: %d\n", len(s1), cap(s1))
	fmt.Printf("是否为nil: %t\n", s1 == nil)
	fmt.Printf("底层指针地址: %#x\n\n", *(*uintptr)(unsafe.Pointer(&s1)))

	fmt.Println("========================")
	fmt.Println("③ s2 := make([]int, 10) → 长度=容量")
	s2 := make([]int, 10)
	fmt.Printf("切片: %v\n", s2)
	fmt.Printf("len: %d, cap: %d\n", len(s2), cap(s2))
	fmt.Printf("是否为nil: %t\n", s2 == nil)
	fmt.Printf("底层指针地址: %#x\n\n", *(*uintptr)(unsafe.Pointer(&s2)))

	fmt.Println("========================")
	fmt.Println("④ s3 := make([]int, 0, 10) → 预分配容量")
	s3 := make([]int, 0, 10)
	fmt.Printf("切片: %v\n", s3)
	fmt.Printf("len: %d, cap: %d\n", len(s3), cap(s3))
	fmt.Printf("是否为nil: %t\n", s3 == nil)
	fmt.Printf("底层指针地址: %#x\n", *(*uintptr)(unsafe.Pointer(&s3)))
}

✅ 运行结果(真实输出)
复制代码
========================
① var s []int → nil切片
切片: []
len: 0, cap: 0
是否为nil: true
底层指针地址: 0x0

========================
② s1 := make([]int, 0) → 零长度切片
切片: []
len: 0, cap: 0
是否为nil: false
底层指针地址: 0x1400001a080

========================
③ s2 := make([]int, 10) → 长度=容量
切片: [0 0 0 0 0 0 0 0 0 0]
len: 10, cap: 10
是否为nil: false
底层指针地址: 0x140000a2000

========================
④ s3 := make([]int, 0, 10) → 预分配容量
切片: []
len: 0, cap: 10
是否为nil: false
底层指针地址: 0x140000a2050

🔍 结果极简说明
  1. nil 切片地址是 0x0,真正的空,无底层数组;
  2. make([]int, 0) 有地址、有数组,只是长度为 0;
  3. make([]int, 10) 直接创建 10 个元素,自动赋 0;
  4. make([]int, 0, 10) 预分配空间,append 不扩容,性能最高

六、切片的操作

(一)访问元素

切片元素的访问方式与数组相似,借助索引获取或修改元素值。索引从 0 起始,最大索引为切片长度减 1。例如:

go 复制代码
package main

import "fmt"

func main() {
    slice := []int{10, 20, 30}
    // 获取第一个元素
    firstElement := slice[0] 
    fmt.Println("第一个元素:", firstElement) 
    // 修改第二个元素
    slice[1] = 25 
    fmt.Println("修改后的切片:", slice) 
}

运行结果说明:程序先定义切片 slice,接着获取其首个元素并打印,随后修改第二个元素的值并打印修改后的切片。程序输出为:

复制代码
第一个元素: 10
修改后的切片: [10 25 30]

(二)遍历切片

与数组相同,我们可利用 for 循环遍历切片,常见方式有普通 for 循环与 for - range 循环。

  • 普通 for 循环遍历
go 复制代码
package main

import "fmt"

func main() {
    slice := []int{10, 20, 30}
    for i := 0; i < len(slice); i++ {
        fmt.Printf("索引 %d 处的元素: %d\n", i, slice[i])
    }
}

运行结果说明:通过普通 for 循环遍历切片 slice,逐个输出各元素的索引与值。程序输出为:

复制代码
索引 0 处的元素: 10
索引 1 处的元素: 20
索引 2 处的元素: 30
  • for - range 循环遍历
go 复制代码
package main

import "fmt"

func main() {
    slice := []int{10, 20, 30}
    for index, value := range slice {
        fmt.Printf("索引 %d 对应的值: %d\n", index, value)
    }
}

运行结果说明:运用 for - range 循环遍历切片 slice,同时获取并打印每个元素的索引与值。程序输出与普通 for 循环遍历类似:

复制代码
索引 0 对应的值: 10
索引 1 对应的值: 20
索引 2 对应的值: 30

(三)追加元素

切片的显著优势之一是能够动态追加元素,我们借助 append 函数实现此操作。例如:

go 复制代码
package main

import "fmt"

func main() {
    slice := []int{10, 20}
    // 追加一个元素
    newSlice := append(slice, 30) 
    fmt.Println("追加元素后的切片:", newSlice) 
    // 追加多个元素
    newSlice = append(newSlice, 40, 50) 
    fmt.Println("再次追加元素后的切片:", newSlice) 
}

运行结果说明:程序先定义切片 slice,接着使用 append 函数追加一个元素,然后再次追加多个元素,并分别打印每次追加后的切片。程序输出为:

复制代码
追加元素后的切片: [10 20 30]
再次追加元素后的切片: [10 20 30 40 50]

这里追加元素有很多注意事项,其原理我们将在下一章详细介绍。

七、切片与数组的关系

切片构建于数组之上,是对数组的封装,为我们提供了更为灵活的操作方式。切片自身并不存储数据,而是依托底层数组存储,切片仅是对数组部分内容的引用。

数组与切片的区别主要体现在以下方面:

  • 长度可变:数组长度一经确定便不可更改;切片长度则可按需动态增减。
  • 内存分配:数组在声明时即确定所需内存空间;切片的内存分配更为灵活,随元素增加自动扩容。
  • 赋值和传递:数组属于值类型,赋值与函数传参时会复制整个数组;切片是引用类型,赋值和传参时传递的是切片结构体(含指针、长度和容量),而非底层数组的副本,效率更高。

八、切片的注意事项

(一)切片越界

与数组相同,切片访问也严禁越界。切片的有效索引范围是从 0 到切片长度减 1。若访问超出此范围,程序运行时将引发 panic。例如:

go 复制代码
package main

import "fmt"

func main() {
    slice := []int{10, 20, 30}
    // 以下代码会导致越界错误
    // outOfRange := slice[3] 
    // 若取消注释,运行时会报错:panic: runtime error: index out of range [3] with length 3
}

运行结果说明:若取消注释试图访问越界索引 3,程序将引发运行时错误,提示索引超出范围。当前代码注释掉了越界访问部分,程序正常运行无输出。

(二)切片的内存管理

尽管切片的自动扩容机制带来诸多便利,但内存管理仍需留意。频繁的扩容操作可能引发性能问题,因为每次扩容都涉及内存重新分配与数据复制。实际应用中,若能预先估算切片所需大致容量,创建切片时指定合适容量,可减少不必要的扩容。

九、总结

通过本文,我们全方位了解了Go语言中的切片,涵盖切片的结构、空切片特性,以及 make 函数创建切片的不同形式。切片作为基于数组的动态数据结构,具备灵活的长度调整、高效的内存管理及便捷的操作方式。我们学习了切片的创建、结构、操作方法,以及它与数组的关系和使用注意事项。

下一篇

下一篇,我们将深入探讨Go语言切片的扩容机制与策略、追加元素的注意事项,进一步丰富对切片的认知与运用。

关注我,点赞👍、收藏⭐本篇内容,我们一同在后续博客中持续探索Go语言切片的更多奥秘。

技术标签:#Go语言 #切片 #数据结构 #编程入门 #动态数组 #nil切片

相关推荐
yyy(十一月限定版)1 小时前
数电1对应latex代码
算法
我头发多我先学1 小时前
C++ 红黑树:从规则到实现,手把手带你写一棵红黑树
数据结构·c++·算法
nlpming1 小时前
opencode SQLite 数据库结构与查询手册
算法
Cando学算法1 小时前
中位数定理:到所有点的距离之和最小的点就是中位数
c++·算法·学习方法
nlpming2 小时前
opencode 上下文压缩(Compaction)机制
算法
anew___2 小时前
算法刷题避坑指南:从数据规模到易错点的实战总结
算法
HZY1618yzh2 小时前
洛谷题解:P16304 [蓝桥杯 2026 省 Java C 组] 抽奖活动
java·c++·算法·蓝桥杯
智者知已应修善业2 小时前
【51单片机从奇数始再转偶数逐一点亮并循环】2023-9-8
c++·经验分享·笔记·算法·51单片机
倔强的猴子(翻版)2 小时前
我用 Python 写了个排序库,一亿数据量下比 C 级 np.sort() 快 7 倍
人工智能·python·算法·阿里云·文心一言