404 Not Found

404 Not Found


nginx

数组与切片

数组与切片

想象你去超市买了一整箱牛奶——数组就像这个固定容量的箱子,装满了就不能再多放;而切片则像一个可以不断扩展的购物袋,东西多了就换个更大的袋子。在Go语言中,数组(Array)长度固定、是值类型;切片(Slice)长度可变、是引用类型。实际开发中,切片使用得远比数组多,但理解数组是理解切片的基础。


1. 核心概念

概念 说明
数组 [N]T 长度固定,声明时必须指定大小;是值类型,赋值和传参会复制整个数组
切片 []T 长度可变,底层引用一个数组;是引用类型,赋值和传参共享底层数组
make([]T, len, cap) 创建切片的推荐方式,可指定长度和容量
append(slice, elems...) 向切片追加元素,返回新切片;可能触发底层数组扩容
copy(dst, src) 将 src 切片的内容复制到 dst,返回复制的元素个数
长度 len() 切片中实际包含的元素个数
容量 cap() 切片底层数组从切片起始位置到末尾的元素个数

2. 基本语法/用法

数组声明与初始化

GO
// 声明一个长度为5的int数组,零值初始化
var arr1 [5]int

// 声明并初始化
var arr2 = [5]int{1, 2, 3, 4, 5}

// 让编译器自动推断长度
arr3 := [...]int{10, 20, 30}

// 指定索引初始化
arr4 := [5]int{1: 100, 3: 300} // [0, 100, 0, 300, 0]

切片声明与初始化

GO
// 声明一个nil切片(零值为nil,但可以直接append)
var s1 []int

// 字面量初始化
s2 := []int{1, 2, 3}

// 从数组创建切片
arr := [5]int{10, 20, 30, 40, 50}
s3 := arr[1:4] // [20, 30, 40]

// 使用make创建切片
s4 := make([]int, 5)      // 长度5,容量5
s5 := make([]int, 3, 10)  // 长度3,容量10
💡 提示: nil切片和空切片([]int{}make([]int, 0))在功能上是等价的——lencap都为0,append都能正常使用。区别在于nil切片的JSON序列化结果是null,空切片则是[]

切片操作

GO
s := []int{10, 20, 30, 40, 50}

// 截取子切片 [左闭右开)
s1 := s[1:3]   // [20, 30]
s2 := s[:3]    // [10, 20, 30],从头开始
s3 := s[2:]    // [30, 40, 50],到末尾
s4 := s[:]     // [10, 20, 30, 40, 50],完整切片

// 长度与容量
fmt.Println(len(s))  // 5
fmt.Println(cap(s))  // 5

// append追加元素
s = append(s, 60)            // 追加单个元素
s = append(s, 70, 80, 90)    // 追加多个元素
s = append(s, []int{100}...) // 追加另一个切片

// copy复制切片
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // dst = [1, 2, 3], n = 3
💡 提示: append返回的是新切片,必须接收返回值。当len(s) == cap(s)时,append会分配新的底层数组,此时新旧切片不再共享数据。

💡 提示: 使用s[low:high]截取时,新切片与原切片共享底层数组。修改子切片的元素会影响原切片。如需独立副本,请使用copy


3. 示例代码

示例 1:基础用法(难度⭐)

GO
package main

import "fmt"

func main() {
	// ========== 数组 ==========
	// 声明并初始化一个包含5个成绩的数组
	scores := [5]int{90, 85, 78, 92, 88}
	fmt.Println("成绩数组:", scores)

	// 通过索引访问和修改元素
	fmt.Println("第一个成绩:", scores[0])
	scores[2] = 80 // 将第三个成绩改为80
	fmt.Println("修改后:", scores)

	// 数组遍历:传统for循环
	fmt.Println("\n--- 传统for遍历 ---")
	for i := 0; i < len(scores); i++ {
		fmt.Printf("第%d个成绩: %d\n", i+1, scores[i])
	}

	// 数组遍历:range遍历
	fmt.Println("\n--- range遍历 ---")
	for index, value := range scores {
		fmt.Printf("索引%d: %d\n", index, value)
	}

	// ========== 切片 ==========
	// 从数组派生切片
	top3 := scores[0:3] // 前3个成绩
	fmt.Println("\n前3个成绩:", top3)

	// 字面量创建切片
	fruits := []string{"苹果", "香蕉", "橙子"}
	fmt.Println("水果切片:", fruits)

	// 使用append添加元素
	fruits = append(fruits, "葡萄")
	fmt.Println("添加葡萄后:", fruits)

	// 长度与容量
	fmt.Printf("长度: %d, 容量: %d\n", len(fruits), cap(fruits))
}
▶ 试一试

运行结果:

TEXT
成绩数组: [90 85 78 92 88]
第一个成绩: 90
修改后: [90 85 80 92 88]

--- 传统for遍历 ---
第1个成绩: 90
第2个成绩: 85
第3个成绩: 80
第4个成绩: 92
第5个成绩: 88

--- range遍历 ---
索引0: 90
索引1: 85
索引2: 80
索引3: 92
索引4: 88

前3个成绩: [90 85 80]
水果切片: [苹果 香蕉 橙子]
添加葡萄后: [苹果 香蕉 橙子 葡萄]
长度: 4, 容量: 4

示例 2:进阶用法(难度⭐⭐)

GO
package main

import "fmt"

func main() {
	// ========== append与扩容 ==========
	// 创建一个容量为3的切片
	s := make([]int, 0, 3)
	fmt.Printf("初始: len=%d, cap=%d, %v\n", len(s), cap(s), s)

	// 逐个追加,观察容量变化
	for i := 1; i <= 5; i++ {
		s = append(s, i)
		fmt.Printf("添加%d后: len=%d, cap=%d, %v\n", i, len(s), cap(s), s)
	}

	// ========== 切片共享底层数组 ==========
	original := []int{10, 20, 30, 40, 50}
	sub := original[1:3] // [20, 30]

	fmt.Println("\n--- 共享底层数组演示 ---")
	fmt.Println("原始切片:", original)
	fmt.Println("子切片:  ", sub)

	// 修改子切片会影响原始切片
	sub[0] = 999
	fmt.Println("\n修改子切片后:")
	fmt.Println("原始切片:", original) // original[1]也被改了
	fmt.Println("子切片:  ", sub)

	// ========== 使用copy创建独立副本 ==========
	fmt.Println("\n--- 使用copy独立复制 ---")
	original = []int{10, 20, 30, 40, 50}
	// 先截取子切片,再用copy复制一份独立的
	subSlice := original[1:4] // [20, 30, 40]
	independent := make([]int, len(subSlice))
	copy(independent, subSlice)

	independent[0] = 888
	fmt.Println("原始切片:", original)       // 不受影响
	fmt.Println("独立副本:", independent)     // 只有副本变了

	// ========== 合并两个切片 ==========
	fmt.Println("\n--- 合并切片 ---")
	a := []int{1, 2, 3}
	b := []int{4, 5, 6}
	merged := append(a, b...)
	fmt.Println("合并结果:", merged)
}
▶ 试一试

运行结果:

TEXT
初始: len=0, cap=3, []
添加1后: len=1, cap=3, [1]
添加2后: len=2, cap=3, [1 2]
添加3后: len=3, cap=3, [1 2 3]
添加4后: len=4, cap=6, [1 2 3 4]
添加5后: len=5, cap=6, [1 2 3 4 5]

--- 共享底层数组演示 ---
原始切片: [10 20 30 40 50]
子切片:   [20 30]

修改子切片后:
原始切片: [10 999 30 40 50]
子切片:   [999 30]

--- 使用copy独立复制 ---
原始切片: [10 20 30 40 50]
独立副本: [888 30 40]

--- 合并切片 ---
合并结果: [1 2 3 4 5 6]

示例 3:综合应用(难度⭐⭐⭐)

GO
package main

import "fmt"

// removeElement 从切片中删除指定索引的元素(保持顺序)
func removeElement(s []int, index int) []int {
	if index < 0 || index >= len(s) {
		return s // 索引越界,返回原切片
	}
	// 用append将index前后的部分拼接
	return append(s[:index], s[index+1:]...)
}

// insertElement 在指定索引处插入元素
func insertElement(s []int, index int, value int) []int {
	if index < 0 || index > len(s) {
		return s
	}
	// 先扩展切片,再将元素后移,最后赋值
	s = append(s, 0)               // 追加一个占位元素
	copy(s[index+1:], s[index:])   // 将index及之后的元素后移一位
	s[index] = value               // 在目标位置赋值
	return s
}

// filterEven 过滤出偶数,返回新切片
func filterEven(s []int) []int {
	result := make([]int, 0, len(s)/2) // 预估容量为一半
	for _, v := range s {
		if v%2 == 0 {
			result = append(result, v)
		}
	}
	return result
}

// 测试切片的内存共享与扩容机制
func sliceInternals() {
	fmt.Println("=== 切片底层原理演示 ===")

	// 创建一个底层数组
	data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

	// 截取子切片(共享底层数组)
	s1 := data[2:5] // [2, 3, 4], len=3, cap=8
	s2 := s1[1:3]   // [3, 4], len=2, cap=7

	fmt.Printf("data: %v, len=%d, cap=%d\n", data, len(data), cap(data))
	fmt.Printf("s1:   %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
	fmt.Printf("s2:   %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))

	// s2修改元素会影响s1和data
	s2[0] = 999
	fmt.Printf("\n修改s2[0]=999后:\n")
	fmt.Printf("data: %v\n", data) // data[4]被改为999
	fmt.Printf("s1:   %v\n", s1)   // s1[2]被改为999
	fmt.Printf("s2:   %v\n", s2)   // s2[0]被改为999

	// append可能导致断开关联
	fmt.Println("\n--- append导致扩容 ---")
	s3 := data[0:2] // [0, 1], len=2, cap=10
	fmt.Printf("append前 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))

	// append未超过容量,仍共享
	s3 = append(s3, 99)
	fmt.Printf("append(99)后 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
	fmt.Printf("data[2] = %d (被修改了!)\n", data[2])

	// 超过容量后,分配新数组
	s3 = append(s3, 100, 200, 300, 400, 500, 600, 700, 800)
	fmt.Printf("大量append后 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
	fmt.Printf("data不受影响: %v\n", data)
}

func main() {
	// ========== 切片操作实战 ==========
	fmt.Println("=== 切片操作实战 ===")

	// 初始数据
	nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	fmt.Println("原始数据:", nums)

	// 删除索引4的元素(值为5)
	nums = removeElement(nums, 4)
	fmt.Println("删除索引4后:", nums)

	// 在索引2处插入100
	nums = insertElement(nums, 2, 100)
	fmt.Println("在索引2插入100:", nums)

	// 过滤偶数
	evens := filterEven(nums)
	fmt.Println("偶数切片:", evens)

	// ========== 切片作为栈 ==========
	fmt.Println("\n=== 切片模拟栈 ===")
	var stack []int

	// 入栈
	for i := 1; i <= 5; i++ {
		stack = append(stack, i)
		fmt.Printf("入栈 %d -> 栈: %v\n", i, stack)
	}

	// 出栈(从末尾弹出)
	for len(stack) > 0 {
		// 取出最后一个元素
		top := stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		fmt.Printf("出栈 %d -> 栈: %v\n", top, stack)
	}

	// ========== 底层原理演示 ==========
	fmt.Println()
	sliceInternals()
}
▶ 试一试

运行结果:

TEXT
=== 切片操作实战 ===
原始数据: [1 2 3 4 5 6 7 8 9 10]
删除索引4后: [1 2 3 4 6 7 8 9 10]
在索引2插入100: [1 2 100 3 4 6 7 8 9 10]
偶数切片: [2 100 4 6 8 10]

=== 切片模拟栈 ===
入栈 1 -> 栈: [1]
入栈 2 -> 栈: [1 2]
入栈 3 -> 栈: [1 2 3]
入栈 4 -> 栈: [1 2 3 4]
入栈 5 -> 栈: [1 2 3 4 5]
出栈 5 -> 栈: [1 2 3 4]
出栈 4 -> 栈: [1 2 3]
出栈 3 -> 栈: [1 2]
出栈 2 -> 栈: [1]
出栈 1 -> 栈: []

=== 切片底层原理演示 ===
data: [0 1 2 3 4 5 6 7 8 9], len=10, cap=10
s1:   [2 3 4], len=3, cap=8
s2:   [3 4], len=2, cap=7

修改s2[0]=999后:
data: [0 1 2 3 999 5 6 7 8 9]
s1:   [2 3 999]
s2:   [3 999]

--- append导致扩容 ---
append前 s3: [0 1], len=2, cap=10
append(99)后 s3: [0 1 99], len=3, cap=10
data[2] = 99 (被修改了!)
大量append后 s3: [0 1 99 100 200 300 400 500 600 700 800], len=12, cap=20
data不受影响: [0 1 99 3 999 5 6 7 8 9]

3. 常见应用场景

场景1:批量处理数据(过滤与转换)

GO
package main

import "fmt"

// 将字符串切片中的空字符串过滤掉,并转为大写
func cleanData(input []string) []string {
	result := make([]string, 0, len(input))
	for _, s := range input {
		if s != "" {
			// 转大写(简化示例,实际可用strings.ToUpper)
			upper := ""
			for _, c := range s {
				if c >= 'a' && c <= 'z' {
					upper += string(c - 32)
				} else {
					upper += string(c)
				}
			}
			result = append(result, upper)
		}
	}
	return result
}

func main() {
	raw := []string{"hello", "", "world", "", "go", "lang"}
	cleaned := cleanData(raw)
	fmt.Println("清洗后:", cleaned) // [HELLO WORLD GO LANG]
}

场景2:实现动态队列

GO
package main

import "fmt"

// Queue 使用切片实现简单的FIFO队列
type Queue struct {
	items []string
}

// Enqueue 入队
func (q *Queue) Enqueue(item string) {
	q.items = append(q.items, item)
}

// Dequeue 出队
func (q *Queue) Dequeue() (string, bool) {
	if len(q.items) == 0 {
		return "", false
	}
	item := q.items[0]
	q.items = q.items[1:] // 移除第一个元素
	return item, true
}

// Size 队列大小
func (q *Queue) Size() int {
	return len(q.items)
}

func main() {
	q := &Queue{}
	q.Enqueue("任务A")
	q.Enqueue("任务B")
	q.Enqueue("任务C")
	fmt.Printf("队列大小: %d\n", q.Size())

	for q.Size() > 0 {
		item, _ := q.Dequeue()
		fmt.Println("处理:", item)
	}
}

❓ 常见问题

Q1:var s []int 声明的切片是nil切片,能直接使用吗?

可以。nil切片的lencap都为0,append操作完全正常。Go官方建议:如果切片可能为nil,使用前不需要额外的nil检查,直接append即可。

GO
var s []int           // nil切片
s = append(s, 1, 2)   // 完全合法
fmt.Println(s)        // [1 2]

Q2:切片截取s[2:5]会影响原切片吗?

会。截取产生的子切片与原切片共享底层数组,修改子切片的元素会反映到原切片。如果需要独立副本,使用copy

GO
original := []int{1, 2, 3, 4, 5}
sub := make([]int, 3)
copy(sub, original[2:5]) // 独立副本,互不影响

Q3:append后旧切片的值为什么变了?

len(s) < cap(s)时,append直接在底层数组上写入新元素,不会创建新数组。如果此时有其他切片引用了同一底层数组的后续位置,就会看到"意外"的修改。解决方法:在append前用copy创建独立副本。

Q4:切片的容量是怎么增长的?

Go运行时根据切片当前容量决定新容量。通常策略是:容量小于1024时翻倍增长,大于1024时增长约1.25倍。具体策略可能随Go版本变化,不要依赖精确的增长规则。


📖 小节


📝 作业

练习1(⭐)

编写程序,创建一个包含10个整数的切片(值为1~10),然后:

  1. 打印切片的长度和容量
  2. 截取索引2到7的子切片,打印其内容、长度和容量
  3. 使用range遍历子切片,打印每个元素及其索引

练习2(⭐⭐)

编写一个unique函数,接收一个[]int切片,返回去重后的新切片(保持元素首次出现的顺序)。例如输入[1, 3, 2, 3, 1, 4, 2]应返回[1, 3, 2, 4]

提示:可以用一个辅助切片记录已经出现过的元素。

练习3(⭐⭐⭐)

编写一个mergeSorted函数,接收两个已排序[]int切片,合并为一个排序切片。要求:


下一课

下一课:Map 映射 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏