Golang语言入门之数组、切片与子切片
一、数组
先记住数组的核心特点:盒子大小一旦定了就改不了(长度固定),但盒子里的东西能换(元素值可变)。就像你买了个能装 3 个苹果的铁皮盒,想多装 1 个都不行,但里面的苹果可以换成橘子。
1.1 数组的定义:必须指定 “盒子容量”
Golang 里数组的 “容量”(长度)是类型的一部分,比如 [3]int
和 [5]int
是完全不同的类型。
package mainimport "fmt"func main() {// 定义数组的 3 种方式var a1 [3]int // 方式 1:声明变量,指定长度a2 := [3]int{1, 2, 3} // 方式 2:初始化时指定长度和元素a3 := [...]int{1, 2, 3} // 方式 3:省略长度,由编译器推导fmt.Println("a1:", a1) // 输出:a1: [0 0 0](默认初始化为零值)fmt.Println("a2:", a2) // 输出:a2: [1 2 3]fmt.Println("a3:", a3) // 输出:a3: [1 2 3]
}
坑点:数组不能像 Python 列表那样 “动态加元素”,比如 a0[3] = 4
会报错 —— 因为 [3]int
只有 0、1、2 三个索引。
1.2 数组是 “值传递”:赋值会拷贝整个盒子
这是数组最容易踩的坑!当你把一个数组赋值给另一个变量时,Golang 会复制整个数组的内容,而不是共享同一个盒子。看代码:
func main() {// 1. 先理解普通变量的值传递(类比数组值传递)var b = 10 // b在内存中占8字节(64位系统int),地址&bb1 := b // 拷贝b的值(10)到新内存地址&b1,b和b1是独立变量b = 20 // 修改b的值,只影响b的内存地址,b1不变// 打印对比:地址不同,值不同fmt.Printf("b: 值=%v, 地址=%p\n", b, &b) // 输出:b: 值=20, 地址=0xc0000a6058fmt.Printf("b1: 值=%v, 地址=%p\n", b1, &b1) // 输出:b1: 值=10, 地址=0xc0000a6070// 2. 数组的值传递(核心:拷贝整个数组的所有元素,不是地址)a0 := [3]int{1, 2, 3} // a0占用24字节(3个int,每个8字节),地址&a0a1 := a0 // 拷贝a0的24字节到新地址&a1,a0和a1是独立数组a0[0] = 100 // 修改a0[0](地址&a0[0]),不影响a1[0](地址&a1[0])// 打印数组整体信息:地址不同(数组对象地址)fmt.Printf("a0: 值=%v, 数组地址=%p, 长度=%d\n", a0, &a0, len(a0)) // 输出:a0: 值=[100 2 3], 数组地址=0xc0000a8000, 长度=3fmt.Printf("a1: 值=%v, 数组地址=%p, 长度=%d\n", a1, &a1, len(a1)) // 输出:a1: 值=[1 2 3], 数组地址=0xc0000a8018, 长度=3// 打印数组元素地址:每个元素地址都不同(证明拷贝了所有元素)fmt.Printf("a0[0]地址=%p, a0[1]地址=%p, a0[2]地址=%p\n", &a0[0], &a0[1], &a0[2])// 输出:a0[0]地址=0xc0000a8000, a0[1]地址=0xc0000a8008, a0[2]地址=0xc0000a8010fmt.Printf("a1[0]地址=%p, a1[1]地址=%p, a1[2]地址=%p\n", &a1[0], &a1[1], &a1[2])// 输出:a1[0]地址=0xc0000a8018, a1[1]地址=0xc0000a8020, a1[2]地址=0xc0000a8028
}
总结:数组赋值 = 拷贝新数组,修改一个不会影响另一个。如果数组很大(比如 10 万元素),这种拷贝会浪费内存,这也是为什么 Golang 更常用切片的原因。
1.3 数组的内存结构:顺序存储,首地址 = 第一个元素地址
数组的元素在内存中是 “挨在一起” 存储的,没有空隙。比如 [3]int
在 64 位系统中,每个 int
占 8 字节,整个数组占 24 字节,且数组的地址等于第一个元素的地址:
特殊情况:字符串数组的内存
如果数组元素是字符串(比如 [5]string
),内存结构会不一样:数组中存的不是字符串本身,而是 “字符串的指针 + 长度”(共 16 字节),因为字符串长度不固定,直接存在数组里会 “撑爆”。看代码:
func main() {// 1. int数组的内存结构(值直接存储在数组内存中)a0 := [3]int{1, 2, 3} // 64位系统:每个int=8字节,数组总大小=3*8=24字节fmt.Printf("数组a0的地址: %p\n", &a0) // 数组地址=第一个元素地址fmt.Printf("a0[0]的地址: %p(值=%d)\n", &a0[0], a0[0]) // 第一个元素地址fmt.Printf("a0[1]的地址: %p(值=%d)\n", &a0[1], a0[1]) // 比a0[0]大8字节(连续)fmt.Printf("a0[2]的地址: %p(值=%d)\n", &a0[2], a0[2]) // 比a0[1]大8字节(连续)// 运行结果(地址规律):// 数组a0的地址: 0xc0000a8000// a0[0]的地址: 0xc0000a8000(值=1) → 和数组地址相同// a0[1]的地址: 0xc0000a8008(值=2) → 0xc0000a8000 + 8// a0[2]的地址: 0xc0000a8010(值=3) → 0xc0000a8008 + 8// 2. string数组的内存结构(数组中存“指针+长度”,不是字符串本身)// 字符串在Golang中是结构体:type string struct { ptr *byte; len int } → 共16字节(8+8)aStr := [3]string{"a", // 长度1,指针指向存储'a'的内存"bc", // 长度2,指针指向存储'b'+'c'的内存"你好", // 长度2(rune数),但字节数3(UTF-8),指针指向对应字节}fmt.Println("\nstring数组元素地址(间隔16字节):")for i := 0; i < 3; i++ {fmt.Printf("aStr[%d]: 值=%s, 地址=%p, 字符串长度(字节数)=%d\n", i, aStr[i], &aStr[i], len(aStr[i]))}// 运行结果(地址间隔16字节):// aStr[0]: 值=a, 地址=0xc0000d0000, 字符串长度(字节数)=1// aStr[1]: 值=bc, 地址=0xc0000d0010(0xc0000d0000 + 16), 字符串长度(字节数)=2// aStr[2]: 值=你好, 地址=0xc0000d0020(0xc0000d0010 + 16), 字符串长度(字节数)=6// 验证:string数组元素地址间隔=16字节(指针8+长度8)fmt.Printf("aStr[0]到aStr[1]地址差: %d字节\n", &aStr[1] - &aStr[0]) // 输出:aStr[0]到aStr[1]地址差: 16字节
}
1.4 数组的常用操作:访问、修改、遍历
访问元素:用数组
[索引]
,索引从 0 开始,最后一个元素的索引是len(数组)-1
(避免越界)。修改元素:直接赋值
数组[索引] = 新值
(元素值可变,地址不变)。遍历元素:用
for
循环或for range
(推荐后者,更简洁)。
代码示例:
func main() {a7 := [9]int{100, 200, 300, 400, 500, 600, 700, 800, 900}// 1. 元素访问:索引从0开始,最后一个元素=len(a7)-1(通用写法,避免硬编码)fmt.Println("a7的长度:", len(a7)) // 输出:9fmt.Println("第一个元素(索引0):", a7[0]) // 输出:100fmt.Println("最后一个元素(索引len-1):", a7[len(a7)-1]) // 输出:900fmt.Println("倒数第二个元素(索引len-2):", a7[len(a7)-2]) // 输出:800// 2. 元素修改:修改值,元素地址不变(数组内存固定,只换“内容”)fmt.Printf("修改前:a7[8]值=%d, 地址=%p\n", a7[8], &a7[8]) // 输出:修改前:a7[8]值=900, 地址=0xc0000b0048a7[8] = 999 // 修改值fmt.Printf("修改后:a7[8]值=%d, 地址=%p\n", a7[8], &a7[8]) // 输出:修改后:a7[8]值=999, 地址=0xc0000b0048(地址不变)// 3. for循环遍历(适合需要索引控制的场景,如跳过某些元素)fmt.Println("\nfor循环遍历(只打印偶数索引元素):")for i := 0; i < len(a7); i += 2 { // i步长2,只遍历0、2、4、6、8索引fmt.Printf("索引%d: 值=%d\n", i, a7[i])}// 运行结果:// 索引0: 值=100// 索引2: 值=300// 索引4: 值=500// 索引6: 值=700// 索引8: 值=999// 4. for range遍历(适合简单遍历,返回索引i和元素值v)// 注意:v是元素的拷贝,修改v不影响原数组fmt.Println("\nfor range遍历(验证v是拷贝):")for i, v := range a7 {v += 100 // 修改v(拷贝值),原数组不变fmt.Printf("索引%d: 原数组值=%d, 拷贝值v=%d\n", i, a7[i], v)}// 运行结果(原数组值不变,v是修改后的值):// 索引0: 原数组值=100, 拷贝值v=200// 索引1: 原数组值=200, 拷贝值v=300// ...(后续索引同理)
}
二、切片
数组的 “固定大小” 太死板,所以 Golang 设计了切片(slice) —— 它像一个 “魔法盒子”,底层依赖数组,但可以动态调整大小(本质是扩容时换一个更大的底层数组)。
2.1 切片和数组的核心区别
特性 | 数组 | 切片 |
---|---|---|
长度 | 固定(类型的一部分) | 可变(动态调整) |
类型表示 | [n]T (如 [3]int ) | []T (如 []int ) |
底层依赖 | 无(自身就是存储) | 依赖数组(切片是 “视图”) |
赋值行为 | 值传递(拷贝整个数组) | 引用传递(拷贝 “视图信息”) |
2.2 切片定义:3 种方式 + header 结构验证
字面量定义(len=cap);make 定义(指定 len+cap,cap 省略默认 = len);从数组 / 切片派生(子切片基础);header 结构(array 指针、len、cap)的打印验证。
package main
import "fmt"func main() {// 1. 字面量定义切片(类似数组,无长度)s1 := []int{1, 2, 3} // 切片header包含3部分:array指针(指向底层数组)、len(可用元素数)、cap(底层数组容量)fmt.Printf("s1: 值=%v, 类型=%T\n", s1, s1)fmt.Printf("s1: len=%d, cap=%d\n", len(s1), cap(s1)) // len=3, cap=3(底层数组长度=3)fmt.Printf("s1: header地址=%p, 底层数组地址(array指针)=%p\n", &s1, &s1[0])// 运行结果:// s1: 值=[1 2 3], 类型=[]int// s1: len=3, cap=3// s1: header地址=0xc000008030, 底层数组地址(array指针)=0xc0000140d8// 2. make定义切片(推荐,可控制len和cap,避免频繁扩容)// 语法:make([]T, len, cap) → cap必须≥len,否则编译错误s2 := make([]int, 3) // cap省略,默认=len=3,底层数组是[0,0,0]s3 := make([]int, 1, 2) // len=1(可用元素1个),cap=2(底层数组能存2个元素)s4 := make([]string, 2, 5)// 字符串切片,len=2(零值=""),cap=5// 打印s2细节(len=3, cap=3,元素为零值0)fmt.Printf("\ns2: 值=%v, len=%d, cap=%d\n", s2, len(s2), cap(s2)) // 输出:s2: 值=[0 0 0], len=3, cap=3fmt.Printf("s2[0]地址=%p(底层数组地址)\n", &s2[0]) // 输出:s2[0]地址=0xc0000140f0(底层数组独立于s1)// 打印s3细节(len=1,只能访问s3[0],s3[1]虽在底层数组但不可访问)fmt.Printf("\ns3: 值=%v, len=%d, cap=%d\n", s3, len(s3), cap(s3)) // 输出:s3: 值=[0], len=1, cap=2// fmt.Println(s3[1]) // 运行错误:index out of range [1] with length 1(len=1,索引1越界)// 打印s4细节(字符串切片零值为"",每个元素占16字节,底层数组存指针+长度)fmt.Printf("\ns4: 值=%v, len=%d, cap=%d\n", s4, len(s4), cap(s4)) // 输出:s4: 值=["" ""], len=2, cap=5fmt.Printf("s4[0]地址=%p, s4[1]地址=%p(间隔16字节)\n", &s4[0], &s4[1])// 输出:s4[0]地址=0xc0000d0000, s4[1]地址=0xc0000d0010(间隔16字节)// 3. 从数组派生切片(切片底层数组=原数组,header的array指针指向数组起始位置)arr := [5]int{1, 2, 3, 4, 5} // 原数组:len=5, cap=5s5 := arr[1:3] // 切取索引1-2(前包后不包),len=2, cap=5-1=4(cap=原数组cap - start)fmt.Printf("\ns5(从数组派生): 值=%v, len=%d, cap=%d\n", s5, len(s5), cap(s5)) // 输出:s5: 值=[2 3], len=2, cap=4fmt.Printf("s5底层数组地址=%p, 原数组arr[1]地址=%p(相同,证明共享数组)\n", &s5[0], &arr[1])// 输出:s5底层数组地址=0xc000014118, 原数组arr[1]地址=0xc000014118(共享底层数组)
}
2.3 切片引用传递:header 拷贝 + 底层数组共享
切片赋值 / 传参拷贝的是 header(24 字节:array 指针 8+len8+cap8);共享底层数组时修改元素互影响;header 地址不同但 array 指针相同。
// 定义函数:接收切片参数,验证引用传递
func showAddr(arr []int) []int {// 1. 打印函数内切片的header地址和底层数组地址// 重点:&arr是函数内切片的header地址(和main中a0的header地址不同)// &arr[0]是底层数组地址(和main中a0的底层数组地址相同)fmt.Printf("函数内(未append): \n")fmt.Printf(" 切片值=%v, header地址=%p, 底层数组地址=%p\n", arr, &arr, &arr[0])fmt.Printf(" len=%d, cap=%d\n", len(arr), cap(arr))// 2. append元素(未扩容,因为3+2=5 ≤ cap=3?不,原cap=3,3+2=5>3,会扩容)arr = append(arr, 123, 321) // 3. 打印扩容后的数据:底层数组地址变化(新数组),header地址不变(还是函数内的arr header)fmt.Printf("函数内(append后): \n")fmt.Printf(" 切片值=%v, header地址=%p, 底层数组地址=%p\n", arr, &arr, &arr[0])fmt.Printf(" len=%d, cap=%d\n", len(arr), cap(arr))return arr // 返回的是函数内arr的header拷贝(array指针指向新底层数组)
}func main() {// 原切片a0:len=3, cap=3,底层数组[1,2,3]a0 := []int{1, 2, 3}fmt.Printf("main内(初始a0): \n")fmt.Printf(" 切片值=%v, header地址=%p, 底层数组地址=%p\n", a0, &a0, &a0[0])fmt.Printf(" len=%d, cap=%d\n", len(a0), cap(a0))// 运行结果:// main内(初始a0): // 切片值=[1 2 3], header地址=0xc000008030, 底层数组地址=0xc0000140d8// len=3, cap=3// 调用函数:传递a0的header拷贝(array指针=0xc0000140d8, len=3, cap=3)a2 := showAddr(a0)// 打印main内最终状态:a0未变(底层数组还是老的),a2是新切片(底层数组新的)fmt.Printf("\nmain内(最终): \n")fmt.Printf(" a0值=%v, header地址=%p, 底层数组地址=%p, len=%d, cap=%d\n", a0, &a0, &a0[0], len(a0), cap(a0))fmt.Printf(" a2值=%v, header地址=%p, 底层数组地址=%p, len=%d, cap=%d\n", a2, &a2, &a2[0], len(a2), cap(a2))// 运行结果:// main内(最终): // a0值=[1 2 3], header地址=0xc000008030, 底层数组地址=0xc0000140d8, len=3, cap=3// a2值=[1 2 3 123 321], header地址=0xc000008060, 底层数组地址=0xc00000e3f0, len=5, cap=6// 关键结论:// 1. a0和函数内arr的header地址不同(0xc000008030 vs 0xc000008078)→ 证明拷贝了header// 2. 未扩容前,a0和函数内arr的底层数组地址相同(0xc0000140d8)→ 共享底层数组// 3. 扩容后,函数内arr的底层数组地址变了(0xc00000e3f0)→ 新数组// 4. a0的底层数组仍为老地址→ a0不变,a2用新数组→ 两者独立
}
2.4 append 操作:扩容策略 + 不同场景验证
append 未扩容(len 增加,cap 不变,共享底层数组);append 扩容(1.18 + 策略:cap<256 翻倍,cap≥256 按 1.25+192);扩容后底层数组更换。
func main() {// 场景1:append未扩容(len+新增元素数 ≤ cap)s1 := make([]int, 2, 5) // len=2, cap=5,底层数组[0,0,_,_,_](_表示未使用)fmt.Printf("s1初始: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", s1, len(s1), cap(s1), &s1[0])// 输出:s1初始: 值=[0 0], len=2, cap=5, 底层数组地址=0xc0000140d8// append 2个元素(2+2=4 ≤ cap=5,未扩容)s1 = append(s1, 3, 4)fmt.Printf("s1 append后: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", s1, len(s1), cap(s1), &s1[0])// 输出:s1 append后: 值=[0 0 3 4], len=4, cap=5, 底层数组地址=0xc0000140d8(地址不变)// 场景2:append扩容(len+新增元素数 > cap)→ 1.18+扩容策略验证// 子场景2.1:cap<256 → 新cap=原cap*2s2 := make([]int, 3, 3) // 原cap=3 <256,新增2个元素(3+2=5>3)fmt.Printf("\ns2初始: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", s2, len(s2), cap(s2), &s2[0])// 输出:s2初始: 值=[0 0 0], len=3, cap=3, 底层数组地址=0xc0000140f0s2 = append(s2, 100, 200) // 扩容:新cap=3*2=6fmt.Printf("s2 append后: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", s2, len(s2), cap(s2), &s2[0])// 输出:s2 append后: 值=[0 0 0 100 200], len=5, cap=6, 底层数组地址=0xc00000e3f0(地址变了)// 子场景2.2:cap≥256 → 新cap=原cap*1.25 + 192(验证)// 先创建一个cap=256的切片s3 := make([]int, 0, 256)for i := 0; i < 256; i++ {s3 = append(s3, i) // 填充256个元素,len=256, cap=256}fmt.Printf("\ns3初始: len=%d, cap=%d, 底层数组地址=%p\n", len(s3), cap(s3), &s3[0])// 输出:s3初始: len=256, cap=256, 底层数组地址=0xc00008a000// append 1个元素(256+1=257>256,扩容)s3 = append(s3, 256)// 计算新cap:256*1.25 + 192 = 320 + 192 = 512?不,Golang中是整数计算:256 * 5 /4 + 192 = 320 + 192 = 512fmt.Printf("s3 append后: len=%d, cap=%d, 底层数组地址=%p\n", len(s3), cap(s3), &s3[0])// 输出:s3 append后: len=257, cap=512, 底层数组地址=0xc0000c8000(地址变了,cap=512)// 场景3:append多个元素,多次扩容s4 := []int{1} // len=1, cap=1fmt.Printf("\ns4扩容过程:\n")for i := 2; i <= 10; i++ {s4 = append(s4, i)fmt.Printf(" 追加%d后: len=%d, cap=%d, 底层数组地址=%p\n", i, len(s4), cap(s4), &s4[0])}// 运行结果(cap<256,每次扩容翻倍):// s4扩容过程:// 追加2后: len=2, cap=2, 底层数组地址=0xc000014130(原cap=1→2)// 追加3后: len=3, cap=4, 底层数组地址=0xc000014140(原cap=2→4)// 追加4后: len=4, cap=4, 底层数组地址=0xc000014140(未扩容)// 追加5后: len=5, cap=8, 底层数组地址=0xc000014160(原cap=4→8)// 追加6后: len=6, cap=8, 底层数组地址=0xc000014160(未扩容)// 追加7后: len=7, cap=8, 底层数组地址=0xc000014160(未扩容)// 追加8后: len=8, cap=8, 底层数组地址=0xc000014160(未扩容)// 追加9后: len=9, cap=16, 底层数组地址=0xc0000141a0(原cap=8→16)// 追加10后: len=10, cap=16, 底层数组地址=0xc0000141a0(未扩容)
}
2.5 切片实战:数组相邻元素求和
从数组派生切片的思路;切片长度计算(相邻和个数 = 数组长度 - 1);循环赋值的细节。
func main() {// 需求:有数组[1,4,9,16,2,5,10,15],生成新切片,元素是数组相邻2项的和// 步骤1:定义原数组(固定长度8)arr := [8]int{1, 4, 9, 16, 2, 5, 10, 15}fmt.Printf("原数组: 值=%v, 长度=%d, 类型=%T\n", arr, len(arr), arr)// 输出:原数组: 值=[1 4 9 16 2 5 10 15], 长度=8, 类型=[8]int// 步骤2:计算结果切片的长度(关键逻辑)// 相邻2项和的个数 = 数组元素个数 - 1(8个元素→7个和)resultLen := len(arr) - 1 fmt.Printf("结果切片长度: %d\n", resultLen) // 输出:7// 步骤3:创建结果切片(用make,指定len=resultLen,cap默认=resultLen)result := make([]int, resultLen) fmt.Printf("初始化结果切片: 值=%v, len=%d, cap=%d\n", result, len(result), cap(result))// 输出:初始化结果切片: 值=[0 0 0 0 0 0 0], len=7, cap=7(零值切片)// 步骤4:循环计算相邻和(i从0到resultLen-1,共7次)for i := 0; i < resultLen; i++ {// 逻辑:第i个和 = 数组第i个元素 + 数组第i+1个元素sum := arr[i] + arr[i+1]result[i] = sum // 给结果切片的第i个元素赋值(覆盖零值)fmt.Printf("第%d次循环: arr[%d]+arr[%d]=%d+%d=%d → result[%d]=%d\n", i+1, i, i+1, arr[i], arr[i+1], sum, i, sum)}// 循环运行详情:// 第1次循环: arr[0]+arr[1]=1+4=5 → result[0]=5// 第2次循环: arr[1]+arr[2]=4+9=13 → result[1]=13// 第3次循环: arr[2]+arr[3]=9+16=25 → result[2]=25// 第4次循环: arr[3]+arr[4]=16+2=18 → result[3]=18// 第5次循环: arr[4]+arr[5]=2+5=7 → result[4]=7// 第6次循环: arr[5]+arr[6]=5+10=15 → result[5]=15// 第7次循环: arr[6]+arr[7]=10+15=25 → result[6]=25// 步骤5:输出最终结果fmt.Printf("\n最终结果:相邻两项和的新切片: %v\n", result)// 输出:最终结果:相邻两项和的新切片: [5 13 25 18 7 15 25]
}
三、子切片
子切片是从数组或切片中 “切取一部分” 得到的新切片,核心特点:不扩容时共享底层数组,就像给原数组 / 切片开了个 “局部窗口”。
3.1 子切片的语法:slice[start:end]
(前包后不包)
语法规则很简单:
start
:切取的起始索引(默认 0,必须 ≤ end)。end
:切取的结束索引(默认len(原切片)
,必须 ≤cap(原切片)
)。结果切片的 len = end - start。
结果切片的 cap = cap(原切片) - start(底层数组从 start 到末尾的长度)。
func main() {// 基础切片:len=6, cap=6,底层数组[1,2,3,4,5,6](索引0-5)base := []int{1, 2, 3, 4, 5, 6}fmt.Printf("基础切片base: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", base, len(base), cap(base), &base[0])// 输出:基础切片base: 值=[1 2 3 4 5 6], len=6, cap=6, 底层数组地址=0xc0000140d8// 场景1:start缺省(=0),end=3 → base[0:3]sub1 := base[:3] // len=3-0=3,cap=6-0=6(start=0,cap=原cap)fmt.Printf("\nsub1=base[:3]: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sub1, len(sub1), cap(sub1), &sub1[0])// 输出:sub1=base[:3]: 值=[1 2 3], len=3, cap=6, 底层数组地址=0xc0000140d8(共享)// 场景2:end缺省(=len(base)=6),start=2 → base[2:6]sub2 := base[2:] // len=6-2=4,cap=6-2=4(start=2,cap=原cap-2)fmt.Printf("sub2=base[2:]: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sub2, len(sub2), cap(sub2), &sub2[0])// 输出:sub2=base[2:]: 值=[3 4 5 6], len=4, cap=4, 底层数组地址=0xc0000140f0(base[2]地址)// 场景3:start=2, end=5 → base[2:5]sub3 := base[2:5] // len=5-2=3,cap=6-2=4(end=5≤cap=6,合法)fmt.Printf("sub3=base[2:5]: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sub3, len(sub3), cap(sub3), &sub3[0])// 输出:sub3=base[2:5]: 值=[3 4 5], len=3, cap=4, 底层数组地址=0xc0000140f0(共享)// 场景4:start=end=3 → base[3:3](空切片)sub4 := base[3:3] // len=3-3=0,cap=6-3=3(空切片,但有cap,可append)fmt.Printf("sub4=base[3:3]: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sub4, len(sub4), cap(sub4), &sub4[0]) // 注意:空切片&sub4[0]会panic吗?// 运行错误:panic: runtime error: index out of range [0] with length 0(len=0,不能访问[0])// 修正:空切片的底层数组地址需通过append后验证,或用reflect包(新手暂不涉及)fmt.Printf("sub4=base[3:3]: 值=%v, len=%d, cap=%d\n", sub4, len(sub4), cap(sub4))// 输出:sub4=base[3:3]: 值=[], len=0, cap=3// 场景5:end=cap(base)=6,start=4 → base[4:6]sub5 := base[4:6] // len=6-4=2,cap=6-4=2(end=cap,合法)fmt.Printf("sub5=base[4:6]: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sub5, len(sub5), cap(sub5), &sub5[0])// 输出:sub5=base[4:6]: 值=[5 6], len=2, cap=2, 底层数组地址=0xc000014100(base[4]地址)// 场景6:start=cap(base)=6,end=6 → base[6:6](空切片,cap=0)sub6 := base[6:6] // len=6-6=0,cap=6-6=0(cap=0,append会直接扩容)fmt.Printf("sub6=base[6:6]: 值=%v, len=%d, cap=%d\n", sub6, len(sub6), cap(sub6))// 输出:sub6=base[6:6]: 值=[], len=0, cap=0// 场景7:end>cap(base) → 错误(end不能超过cap)// sub7 := base[2:7] // 编译通过,但运行错误:runtime error: slice bounds out of range [:7] with capacity 6// 场景8:start>end → 错误// sub8 := base[4:2] // 编译错误:invalid slice bound: 4 > 2
}
3.2 子切片与原切片:共享底层数组(未扩容)vs 独立(扩容)
未扩容时,修改子切片→影响原切片;修改原切片→影响子切片;扩容后,两者底层数组独立,修改互不影响。
func main() {// 原切片:len=6, cap=6,底层数组[1,2,3,4,5,6]origin := []int{1, 2, 3, 4, 5, 6}fmt.Printf("初始原切片origin: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", origin, len(origin), cap(origin), &origin[0])// 输出:初始原切片origin: 值=[1 2 3 4 5 6], len=6, cap=6, 底层数组地址=0xc0000140d8// 1. 未扩容:子切片与原切片共享底层数组sub := origin[1:4] // len=3, cap=5,底层数组=origin的底层数组(地址相同)fmt.Printf("未扩容子切片sub: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sub, len(sub), cap(sub), &sub[0])// 输出:未扩容子切片sub: 值=[2 3 4], len=3, cap=5, 底层数组地址=0xc0000140e0(origin[1]地址)// 案例1:修改子切片sub的元素 → 原切片origin对应位置变化sub[0] = 200 // sub[0]对应origin[1]fmt.Printf("\n修改sub[0]=200后:\n")fmt.Printf("sub: 值=%v\n", sub) // 输出:sub: 值=[200 3 4]fmt.Printf("origin: 值=%v\n", origin) // 输出:origin: 值=[1 200 3 4 5 6](origin[1]变了)// 案例2:修改原切片origin的元素 → 子切片sub对应位置变化origin[3] = 400 // origin[3]对应sub[2]fmt.Printf("\n修改origin[3]=400后:\n")fmt.Printf("origin: 值=%v\n", origin) // 输出:origin: 值=[1 200 3 400 5 6]fmt.Printf("sub: 值=%v\n", sub) // 输出:sub: 值=[200 3 400](sub[2]变了)// 2. 子切片扩容:底层数组更换,与原切片独立// sub当前len=3, cap=5,append 3个元素(3+3=6>5 → 扩容)sub = append(sub, 500, 600, 700)fmt.Printf("\nsub append后(扩容):\n")fmt.Printf("sub: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sub, len(sub), cap(sub), &sub[0])// 输出:sub: 值=[200 3 400 500 600 700], len=6, cap=10, 底层数组地址=0xc00000e3f0(新地址)fmt.Printf("origin: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", origin, len(origin), cap(origin), &origin[0])// 输出:origin: 值=[1 200 3 400 5 6], len=6, cap=6, 底层数组地址=0xc0000140d8(老地址)// 案例3:扩容后修改sub → 不影响originsub[0] = 2000fmt.Printf("\n扩容后修改sub[0]=2000:\n")fmt.Printf("sub: 值=%v\n", sub) // 输出:sub: 值=[2000 3 400 500 600 700]fmt.Printf("origin: 值=%v\n", origin) // 输出:origin: 值=[1 200 3 400 5 6](不变)// 案例4:扩容后修改origin → 不影响suborigin[1] = 20fmt.Printf("\n扩容后修改origin[1]=20:\n")fmt.Printf("origin: 值=%v\n", origin) // 输出:origin: 值=[1 20 3 400 5 6]fmt.Printf("sub: 值=%v\n", sub) // 输出:sub: 值=[2000 3 400 500 600 700](不变)
}
3.3 从数组派生子切片:修改切片影响数组
数组派生切片后,切片的底层数组 = 原数组;修改切片元素→原数组对应元素变化;数组长度固定,切片 append 扩容后与数组独立。
func main() {// 原数组:len=6, cap=6,内存固定arr := [6]int{1, 2, 3, 4, 5, 6}fmt.Printf("初始数组arr: 值=%v, 类型=%T, 数组地址=%p, arr[1]地址=%p\n", arr, arr, &arr, &arr[1])// 输出:初始数组arr: 值=[1 2 3 4 5 6], 类型=[6]int, 数组地址=0xc0000a8000, arr[1]地址=0xc0000a8008// 1. 从数组派生切片:slice=arr[start:end]sliceFromArr := arr[1:4] // 切取arr[1]、arr[2]、arr[3],len=3, cap=6-1=5fmt.Printf("从数组派生的切片sliceFromArr: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sliceFromArr, len(sliceFromArr), cap(sliceFromArr), &sliceFromArr[0])// 输出:sliceFromArr: 值=[2 3 4], len=3, cap=5, 底层数组地址=0xc0000a8008(和arr[1]地址相同)// 案例1:修改切片元素 → 原数组变化sliceFromArr[0] = 200 // 切片[0]对应arr[1]fmt.Printf("\n修改sliceFromArr[0]=200后:\n")fmt.Printf("sliceFromArr: 值=%v\n", sliceFromArr) // 输出:[200 3 4]fmt.Printf("arr: 值=%v\n", arr) // 输出:[1 200 3 4 5 6](arr[1]变了)// 案例2:修改原数组元素 → 切片变化arr[3] = 400 // arr[3]对应切片[2]fmt.Printf("\n修改arr[3]=400后:\n")fmt.Printf("arr: 值=%v\n", arr) // 输出:[1 200 3 400 5 6]fmt.Printf("sliceFromArr: 值=%v\n", sliceFromArr) // 输出:[200 3 400](切片[2]变了)// 2. 切片append扩容:与原数组独立// 切片当前len=3, cap=5,append 3个元素(3+3=6>5 → 扩容)sliceFromArr = append(sliceFromArr, 500, 600, 700)fmt.Printf("\nsliceFromArr append后(扩容):\n")fmt.Printf("sliceFromArr: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", sliceFromArr, len(sliceFromArr), cap(sliceFromArr), &sliceFromArr[0])// 输出:sliceFromArr: 值=[200 3 400 500 600 700], len=6, cap=10, 底层数组地址=0xc00000e3f0(新地址)fmt.Printf("arr: 值=%v, 数组地址=%p\n", arr, &arr) // 输出:arr: 值=[1 200 3 400 5 6], 数组地址=0xc0000a8000(老地址,数组不变)// 案例3:扩容后修改切片 → 不影响数组sliceFromArr[0] = 2000fmt.Printf("\n扩容后修改sliceFromArr[0]=2000:\n")fmt.Printf("sliceFromArr: 值=%v\n", sliceFromArr) // 输出:[2000 3 400 500 600 700]fmt.Printf("arr: 值=%v\n", arr) // 输出:[1 200 3 400 5 6](不变)
}
3.4 子切片实用技巧:避免共享影响(make+copy)
当需要子切片但不想影响原切片时,用make创建新切片,copy拷贝元素,实现底层数组独立。
func main() {// 原切片:len=5, cap=5,底层数组[10,20,30,40,50]origin := []int{10, 20, 30, 40, 50}fmt.Printf("原切片origin: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", origin, len(origin), cap(origin), &origin[0])// 输出:origin: 值=[10 20 30 40 50], len=5, cap=5, 底层数组地址=0xc0000140d8// 需求:取origin[1:4]的子切片,但修改子切片不影响origin// 方法1:直接切取(共享底层数组,会影响)subShare := origin[1:4]subShare[0] = 200fmt.Printf("\n直接切取subShare修改后:\n")fmt.Printf("subShare: 值=%v\n", subShare) // 输出:[200 30 40]fmt.Printf("origin: 值=%v\n", origin) // 输出:[10 200 30 40 50](被影响)// 恢复origin的值origin[1] = 20fmt.Printf("\n恢复origin后:%v\n", origin) // 输出:[10 20 30 40 50]// 方法2:make+copy(独立底层数组,不影响)// 步骤1:切取子切片(临时,用于获取元素)subTemp := origin[1:4] // 步骤2:创建新切片,len和cap与subTemp相同(或自定义)subIndependent := make([]int, len(subTemp), cap(subTemp)) // 步骤3:copy元素(copy(dst, src),返回拷贝的元素个数)copyCount := copy(subIndependent, subTemp) fmt.Printf("\nmake+copy创建的subIndependent:\n")fmt.Printf("拷贝元素个数: %d\n", copyCount) // 输出:3(subTemp有3个元素)fmt.Printf("subIndependent: 值=%v, len=%d, cap=%d, 底层数组地址=%p\n", subIndependent, len(subIndependent), cap(subIndependent), &subIndependent[0])// 输出:subIndependent: 值=[20 30 40], len=3, cap=4, 底层数组地址=0xc0000140f0(新地址,和origin不同)// 步骤4:修改subIndependent,验证不影响originsubIndependent[0] = 2000fmt.Printf("\n修改subIndependent[0]=2000后:\n")fmt.Printf("subIndependent: 值=%v\n", subIndependent) // 输出:[2000 30 40]fmt.Printf("origin: 值=%v\n", origin) // 输出:[10 20 30 40 50](未被影响)// 关键:copy的细节(拷贝长度取dst和src的较小值)dstShort := make([]int, 2) // dst len=2srcLong := []int{1,2,3,4} // src len=4copyCount2 := copy(dstShort, srcLong)fmt.Printf("\ncopy(dstShort, srcLong):\n")fmt.Printf("拷贝元素个数: %d\n", copyCount2) // 输出:2(取dst和src的较小len)fmt.Printf("dstShort: 值=%v\n", dstShort) // 输出:[1 2](只拷贝前2个元素)
}
四、全知识点对照表
代码示例场景 | 对应核心知识点 | 易错点提醒 |
数组[3]int vs [5]int | 数组长度是类型的一部分,不同长度是不同类型 | 变量不能作为数组长度(必须编译时确定) |
数组赋值a1 := a0 | 数组值传递,拷贝整个数组,修改互不影响 | 大数组赋值浪费内存(推荐用切片) |
切片make([]int,1,2) | 切片 header 含 array 指针 + len+cap,cap≥len | len 是可用元素数,cap 是底层数组容量 |
切片传参showAddr(a0) | 切片引用传递,拷贝 header(24 字节),共享底层数组 | header 地址不同,但 array 指针可能相同 |
切片 append 扩容 | 1.18 + 策略:cap<256 翻倍,cap≥256 按 1.25+192 | 扩容后底层数组更换,原切片不变 |
子切片base[1:4] | len=4-1=3,cap = 原 cap-1,共享底层数组(未扩容) | end 不能超过 cap,start 不能大于 end |
子切片 append 扩容 | 扩容后底层数组独立,与原切片 / 数组互不影响 | 空切片(start=end)也有 cap,可 append |
切片copy(new, sub) | copy 拷贝元素,新切片有独立底层数组,避免共享影响 | copy 长度取 dst 和 src 的较小值 |
string 数组元素地址间隔 16 字节 | string 是结构体(指针 8 + 长度 8),数组存结构体 | 字符串本身存在其他内存,数组存的是引用 |
数组越界a0[3] | 数组长度固定,索引不能超过 len-1,运行时 panic | 用len(a)-1访问最后一个元素,避免硬编码 |