Skip to content

切片

切片(Slice)是 Go 语言中最重要的数据结构之一,提供了动态数组的功能。切片比数组更灵活,是 Go 程序中处理序列数据的首选方式。

📋 学习目标

  • 理解切片的概念和用途
  • 掌握切片的声明和初始化
  • 理解切片的底层实现原理
  • 学会切片的常用操作
  • 掌握切片的扩容机制
  • 理解切片的内存管理
  • 学会使用多维切片

🎯 切片基础

什么是切片

切片是对数组的抽象,提供了动态大小的序列。切片包含三个部分:

  • 指针:指向底层数组
  • 长度(length):切片中元素的数量
  • 容量(capacity):底层数组从切片起始位置到数组末尾的元素数量
go
package main

import "fmt"

func main() {
	// 声明切片(nil 切片)
	var slice1 []int
	fmt.Printf("nil 切片: %v (len=%d, cap=%d, isNil=%t)\n",
		slice1, len(slice1), cap(slice1), slice1 == nil)

	// 直接初始化
	slice2 := []int{1, 2, 3, 4, 5}
	fmt.Printf("直接初始化: %v (len=%d, cap=%d)\n",
		slice2, len(slice2), cap(slice2))

	// 使用 make 创建
	slice3 := make([]int, 5)        // 长度为5,容量为5
	slice4 := make([]int, 3, 10)    // 长度为3,容量为10
	fmt.Printf("make 创建: %v (len=%d, cap=%d)\n",
		slice3, len(slice3), cap(slice3))
	fmt.Printf("make 指定容量: %v (len=%d, cap=%d)\n",
		slice4, len(slice4), cap(slice4))
}

从数组创建切片

go
package main

import "fmt"

func main() {
	// 创建数组
	arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

	// 从数组创建切片
	slice1 := arr[2:7]  // 包含索引2到6的元素
	fmt.Printf("arr[2:7] = %v\n", slice1)

	// 切片的切片
	slice2 := slice1[1:4]  // 从现有切片创建新切片
	fmt.Printf("slice1[1:4] = %v\n", slice2)

	// 完整切片表达式
	slice3 := arr[2:7:8]  // [low:high:max],容量为 max-low
	fmt.Printf("arr[2:7:8] = %v (len=%d, cap=%d)\n",
		slice3, len(slice3), cap(slice3))
}

🔍 切片的底层原理

切片的结构

切片本身不存储数据,而是引用底层数组的一部分。

go
package main

import "fmt"

func main() {
	// 创建一个切片
	s := make([]int, 3, 5)
	s[0], s[1], s[2] = 10, 20, 30

	fmt.Printf("原始切片: %v (len=%d, cap=%d)\n", s, len(s), cap(s))

	// 创建新切片,共享底层数组
	s2 := s[1:3]
	fmt.Printf("新切片: %v (len=%d, cap=%d)\n", s2, len(s2), cap(s2))

	// 修改新切片会影响原切片(共享底层数组)
	s2[0] = 99
	fmt.Printf("修改新切片后:\n")
	fmt.Printf("原切片: %v\n", s)
	fmt.Printf("新切片: %v\n", s2)
}

切片的零值

go
package main

import "fmt"

func main() {
	var s []int
	fmt.Printf("零值切片: %v\n", s)
	fmt.Printf("是否为 nil: %t\n", s == nil)
	fmt.Printf("长度: %d, 容量: %d\n", len(s), cap(s))

	// nil 切片可以安全使用
	s = append(s, 1, 2, 3)
	fmt.Printf("append 后: %v\n", s)
}

🛠️ 切片操作

添加元素(append)

go
package main

import "fmt"

func main() {
	slice := []int{1, 2, 3}
	fmt.Printf("原始切片: %v (len=%d, cap=%d)\n",
		slice, len(slice), cap(slice))

	// 添加单个元素
	slice = append(slice, 4)
	fmt.Printf("添加单个元素: %v (len=%d, cap=%d)\n",
		slice, len(slice), cap(slice))

	// 添加多个元素
	slice = append(slice, 5, 6, 7)
	fmt.Printf("添加多个元素: %v\n", slice)

	// 添加另一个切片
	another := []int{8, 9, 10}
	slice = append(slice, another...)
	fmt.Printf("添加另一个切片: %v\n", slice)
}

删除元素

go
package main

import "fmt"

func main() {
	slice := []int{1, 2, 3, 4, 5}
	fmt.Printf("原始切片: %v\n", slice)

	// 删除索引为 2 的元素
	index := 2
	slice = append(slice[:index], slice[index+1:]...)
	fmt.Printf("删除索引 %d: %v\n", index, slice)

	// 删除第一个元素
	slice = slice[1:]
	fmt.Printf("删除第一个元素: %v\n", slice)

	// 删除最后一个元素
	slice = slice[:len(slice)-1]
	fmt.Printf("删除最后一个元素: %v\n", slice)

	// 删除指定范围的元素
	slice = []int{1, 2, 3, 4, 5, 6, 7, 8}
	slice = append(slice[:2], slice[5:]...)  // 删除索引2到4
	fmt.Printf("删除范围 [2:5]: %v\n", slice)
}

复制切片

go
package main

import "fmt"

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

	// 深拷贝
	copy1 := make([]int, len(original))
	copy(copy1, original)
	fmt.Printf("原切片: %v\n", original)
	fmt.Printf("深拷贝: %v\n", copy1)

	// 修改拷贝不影响原切片
	copy1[0] = 99
	fmt.Printf("修改拷贝后:\n")
	fmt.Printf("原切片: %v\n", original)
	fmt.Printf("深拷贝: %v\n", copy1)

	// 浅拷贝(共享底层数组)
	copy2 := original
	copy2[0] = 88
	fmt.Printf("浅拷贝后:\n")
	fmt.Printf("原切片: %v\n", original)  // 也被修改了
	fmt.Printf("浅拷贝: %v\n", copy2)
}

切片截取

go
package main

import "fmt"

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

	// 截取切片
	slice1 := slice[1:4]    // [2,3,4]
	slice2 := slice[2:]     // [3,4,5]
	slice3 := slice[:3]     // [1,2,3]

	fmt.Printf("原切片: %v\n", slice)
	fmt.Printf("slice[1:4]: %v\n", slice1)
	fmt.Printf("slice[2:]: %v\n", slice2)
	fmt.Printf("slice[:3]: %v\n", slice3)

	// 注意:截取的切片共享底层数组
	slice1[0] = 99
	fmt.Printf("修改 slice1[0] 后:\n")
	fmt.Printf("原切片: %v\n", slice)  // 也被修改了
}

📈 切片扩容

扩容机制

当切片容量不足时,Go 会自动扩容。扩容策略:

  • 如果容量 < 1024,容量翻倍
  • 如果容量 >= 1024,每次增加 25%
go
package main

import "fmt"

func main() {
	var s []int

	fmt.Println("演示切片扩容:")
	for i := 0; i < 20; i++ {
		oldCap := cap(s)
		s = append(s, i)
		newCap := cap(s)

		if oldCap != newCap {
			fmt.Printf("添加 %d: len=%d, cap=%d -> %d (扩容)\n",
				i, len(s), oldCap, newCap)
		}
	}
}

预分配容量

go
package main

import "fmt"

func main() {
	// 不预分配容量
	var s1 []int
	for i := 0; i < 1000; i++ {
		s1 = append(s1, i)
	}
	fmt.Printf("不预分配: 最终 cap=%d\n", cap(s1))

	// 预分配容量(性能更好)
	s2 := make([]int, 0, 1000)
	for i := 0; i < 1000; i++ {
		s2 = append(s2, i)
	}
	fmt.Printf("预分配: 最终 cap=%d\n", cap(s2))
}

💾 内存管理

避免内存泄漏

go
package main

import "fmt"

func main() {
	// 大切片
	large := make([]int, 1000)
	for i := range large {
		large[i] = i
	}

	// 只使用前10个元素
	small := large[:10]
	fmt.Printf("small: len=%d, cap=%d\n", len(small), cap(small))

	// 问题:small 仍然引用整个 large 的底层数组
	// 解决:创建新的切片,只复制需要的元素
	small2 := make([]int, 10)
	copy(small2, large[:10])
	fmt.Printf("small2: len=%d, cap=%d\n", len(small2), cap(small2))

	// 现在 large 可以被垃圾回收
	large = nil
}

切片作为函数参数

go
package main

import "fmt"

// 修改切片(会影响原切片)
func modifySlice(s []int) {
	if len(s) > 0 {
		s[0] = 999
	}
}

// 追加元素(不会影响原切片,除非重新赋值)
func appendToSlice(s []int) {
	s = append(s, 100)
	fmt.Printf("函数内: %v (len=%d, cap=%d)\n", s, len(s), cap(s))
}

func main() {
	slice := []int{1, 2, 3}
	fmt.Printf("原始: %v\n", slice)

	// 修改元素会影响原切片
	modifySlice(slice)
	fmt.Printf("修改后: %v\n", slice)

	// 追加元素不会影响原切片(除非重新赋值)
	appendToSlice(slice)
	fmt.Printf("追加后: %v\n", slice)  // 未改变

	// 需要重新赋值
	slice = append(slice, 200)
	fmt.Printf("重新赋值后: %v\n", slice)
}

🔄 多维切片

二维切片

go
package main

import "fmt"

func main() {
	// 创建二维切片
	matrix := make([][]int, 3)
	for i := range matrix {
		matrix[i] = make([]int, 4)
		for j := range matrix[i] {
			matrix[i][j] = i*4 + j + 1
		}
	}

	fmt.Println("二维切片:")
	for i, row := range matrix {
		fmt.Printf("  行 %d: %v\n", i, row)
	}

	// 不规则的二维切片
	jagged := [][]int{
		{1, 2, 3},
		{4, 5},
		{6, 7, 8, 9},
	}

	fmt.Println("\n不规则二维切片:")
	for i, row := range jagged {
		fmt.Printf("  行 %d: %v\n", i, row)
	}

	// 动态添加行和列
	matrix = append(matrix, []int{13, 14, 15, 16})  // 添加新行
	matrix[0] = append(matrix[0], 17)               // 添加列

	fmt.Println("\n添加后的矩阵:")
	for i, row := range matrix {
		fmt.Printf("  行 %d: %v\n", i, row)
	}
}

🎯 常用操作模式

过滤

go
func filter(slice []int, predicate func(int) bool) []int {
	result := make([]int, 0)
	for _, v := range slice {
		if predicate(v) {
			result = append(result, v)
		}
	}
	return result
}

// 使用
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evens := filter(numbers, func(n int) bool {
	return n%2 == 0
})

映射转换

go
func mapSlice(slice []int, transform func(int) int) []int {
	result := make([]int, len(slice))
	for i, v := range slice {
		result[i] = transform(v)
	}
	return result
}

// 使用
numbers := []int{1, 2, 3, 4, 5}
doubled := mapSlice(numbers, func(n int) int {
	return n * 2
})

查找

go
func contains(slice []int, item int) bool {
	for _, v := range slice {
		if v == item {
			return true
		}
	}
	return false
}

func indexOf(slice []int, item int) int {
	for i, v := range slice {
		if v == item {
			return i
		}
	}
	return -1
}

删除操作

go
// 删除指定位置的元素
slice := []int{1, 2, 3, 4, 5}
i := 2 // 要删除的索引
slice = append(slice[:i], slice[i+1:]...)  // 删除索引i的元素

// 删除指定范围的元素
slice = append(slice[:i], slice[j:]...)    // 删除索引i到j的元素

// 删除并保持顺序(使用copy)
copy(slice[i:], slice[i+1:])
slice = slice[:len(slice)-1]

排序操作

go
import "sort"

// 基本类型排序
numbers := []int{4, 2, 1, 3, 5}
sort.Ints(numbers)               // 升序排序
sort.Sort(sort.Reverse(sort.IntSlice(numbers))) // 降序排序

// 自定义类型排序
type Person struct {
    Name string
    Age  int
}

people := []Person{
    {"Alice", 25},
    {"Bob", 30},
    {"Charlie", 20},
}

sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age
})

搜索操作

go
import "sort"

// 线性查找
slice := []int{1, 2, 3, 4, 5}
for i, v := range slice {
    if v == 3 {
        fmt.Printf("找到元素 %d 在位置 %d\n", v, i)
        break
    }
}

// 二分查找(要求切片已排序)
numbers := []int{1, 2, 3, 4, 5}
index := sort.SearchInts(numbers, 3)  // 返回值为2

🔗 切片与字符串

字符串与切片的转换

go
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	str := "Hello, 世界"

	// 字符串转字节切片
	bytes := []byte(str)
	fmt.Printf("字节切片: %v\n", bytes)

	// 字符串转 rune 切片(Unicode 字符)
	runes := []rune(str)
	fmt.Printf("Rune 切片: %v\n", runes)

	// 切片转字符串
	byteSlice := []byte{'H', 'e', 'l', 'l', 'o'}
	runeSlice := []rune{'', ''}
	str1 := string(byteSlice)
	str2 := string(runeSlice)
	fmt.Printf("字节转字符串: %s\n", str1) // Hello
	fmt.Printf("Rune 转字符串: %s\n", str2) // 世界
}

安全的字符串截取

go
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	str := "Hello, 世界"

	// ❌ 危险:直接截取可能破坏 UTF-8 编码
	substr1 := str[:5] // "Hello" (对于 ASCII 字符是安全的)
	fmt.Printf("直接截取: %s\n", substr1)

	// ✅ 安全:先转换为 rune 切片再截取
	runes := []rune(str)
	substr2 := string(runes[:7]) // 前7个字符
	fmt.Printf("安全截取: %s\n", substr2) // Hello, 世界

	// 获取字符串真实长度
	byteLen := len(str)                      // 字节长度
	charLen := utf8.RuneCountInString(str)   // 字符长度
	fmt.Printf("字节长度: %d, 字符长度: %d\n", byteLen, charLen)

	// 检查 UTF-8 编码
	isValid := utf8.ValidString(str)
	fmt.Printf("有效 UTF-8: %t\n", isValid)
}

字符串与切片的常用操作

go
package main

import (
	"fmt"
	"strconv"
	"strings"
)

func main() {
	// 数字切片转字符串切片
	nums := []int{1, 2, 3, 4, 5}
	strSlice := make([]string, len(nums))
	for i, num := range nums {
		strSlice[i] = strconv.Itoa(num)
	}
	fmt.Printf("数字转字符串: %v\n", strSlice)

	// 字符串切片拼接
	parts := []string{"Hello", "World", "Go"}
	result := strings.Join(parts, " ")
	fmt.Printf("拼接结果: %s\n", result) // Hello World Go

	// 字符串分割为切片
	str := "a,b,c,d"
	parts2 := strings.Split(str, ",")
	fmt.Printf("分割结果: %v\n", parts2) // [a b c d]
}

性能优化技巧

go
package main

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

func main() {
	// 使用 Builder 高效构建字符串
	var builder strings.Builder
	builder.Grow(100) // 预分配内存
	for i := 0; i < 100; i++ {
		builder.WriteString("a")
	}
	result := builder.String()
	fmt.Printf("构建结果长度: %d\n", len(result))

	// 批量字符处理
	str := "Hello, 世界"
	runes := make([]rune, 0, utf8.RuneCountInString(str))
	for _, r := range str {
		runes = append(runes, r)
	}
	fmt.Printf("字符切片: %v\n", runes)
}

实用工具函数

go
package main

import (
	"fmt"
	"unicode"
)

// 安全的子字符串提取
func SubString(s string, start, end int) string {
	runes := []rune(s)
	if start < 0 {
		start = 0
	}
	if end > len(runes) {
		end = len(runes)
	}
	if start > end {
		return ""
	}
	return string(runes[start:end])
}

// 检查字符串是否包含中文字符
func ContainsChinese(s string) bool {
	for _, r := range s {
		if unicode.Is(unicode.Han, r) {
			return true
		}
	}
	return false
}

func main() {
	str := "Hello, 世界"
	substr := SubString(str, 0, 7)
	fmt.Printf("子字符串: %s\n", substr) // Hello, 世界

	hasChinese := ContainsChinese(str)
	fmt.Printf("包含中文: %t\n", hasChinese) // true
}

⚠️ 常见陷阱

1. 在循环中使用 append

go
// ❌ 错误:可能导致意外的行为
var result []int
for i := 0; i < 3; i++ {
	result = append(result, i)
	// 如果 result 在循环外被其他 goroutine 修改,会有问题
}

// ✅ 正确:预分配容量
result := make([]int, 0, 3)
for i := 0; i < 3; i++ {
	result = append(result, i)
}

2. 共享底层数组

go
// 注意:多个切片可能共享底层数组
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:4]  // 共享底层数组

s2[0] = 99
fmt.Println(s1)  // [1, 99, 3, 4, 5] - s1 也被修改了

// 如果需要独立,使用 copy
s3 := make([]int, len(s1))
copy(s3, s1)

3. 切片作为函数参数

go
// 注意:切片是引用类型,但 append 需要重新赋值
func addElement(s []int, val int) {
	s = append(s, val)  // 不会影响原切片
	// 需要返回新切片
}

// 正确做法
func addElement(s []int, val int) []int {
	return append(s, val)
}

🏃‍♂️ 实践练习

练习 1: 实现栈

使用切片实现一个栈数据结构。

练习 2: 切片去重

实现一个函数,去除切片中的重复元素。

练习 3: 切片合并

实现一个函数,合并多个切片。

🤔 思考题

  1. 切片和数组有什么区别?
  2. 切片的扩容机制是什么?
  3. 什么时候应该预分配切片容量?
  4. 如何避免切片的内存泄漏?
  5. 切片作为函数参数时需要注意什么?

📚 扩展阅读

⏭️ 下一章节

映射 → 学习 Go 语言的映射(Map)数据结构


💡 提示: 切片是 Go 语言中最常用的数据结构,理解其底层原理和内存管理对于编写高效的 Go 程序至关重要!

基于 VitePress 构建