Golang语言之数组、切片与子切片

发布于:2025-09-12 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、数组

先记住数组的核心特点:盒子大小一旦定了就改不了(长度固定),但盒子里的东西能换(元素值可变)。就像你买了个能装 3 个苹果的铁皮盒,想多装 1 个都不行,但里面的苹果可以换成橘子。

1.1 数组的定义:必须指定 “盒子容量”

Golang 里数组的 “容量”(长度)是类型的一部分,比如 [3]int[5]int 是完全不同的类型。

package main

import "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),地址&b
    b1 := b             // 拷贝b的值(10)到新内存地址&b1,b和b1是独立变量
    b = 20              // 修改b的值,只影响b的内存地址,b1不变
    // 打印对比:地址不同,值不同
    fmt.Printf("b: 值=%v, 地址=%p\n", b, &b)    // 输出:b: 值=20, 地址=0xc0000a6058
    fmt.Printf("b1: 值=%v, 地址=%p\n", b1, &b1) // 输出:b1: 值=10, 地址=0xc0000a6070

    // 2. 数组的值传递(核心:拷贝整个数组的所有元素,不是地址)
    a0 := [3]int{1, 2, 3} // a0占用24字节(3个int,每个8字节),地址&a0
    a1 := 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, 长度=3
    fmt.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]地址=0xc0000a8010
    fmt.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))                  // 输出:9
    fmt.Println("第一个元素(索引0):", a7[0])           // 输出:100
    fmt.Println("最后一个元素(索引len-1):", a7[len(a7)-1]) // 输出:900
    fmt.Println("倒数第二个元素(索引len-2):", a7[len(a7)-2]) // 输出:800

    // 2. 元素修改:修改值,元素地址不变(数组内存固定,只换“内容”)
    fmt.Printf("修改前:a7[8]值=%d, 地址=%p\n", a7[8], &a7[8]) 
    // 输出:修改前:a7[8]值=900, 地址=0xc0000b0048
    a7[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=3
    fmt.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=5
    fmt.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=5
    s5 := 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=4
    fmt.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*2
    s2 := 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, 底层数组地址=0xc0000140f0

    s2 = append(s2, 100, 200) // 扩容:新cap=3*2=6
    fmt.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 = 512
    fmt.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=1
    fmt.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 → 不影响origin
    sub[0] = 2000
    fmt.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 → 不影响sub
    origin[1] = 20
    fmt.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=5
    fmt.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] = 2000
    fmt.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] = 200
    fmt.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] = 20
    fmt.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,验证不影响origin
    subIndependent[0] = 2000
    fmt.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=2
    srcLong := []int{1,2,3,4}  // src len=4
    copyCount2 := 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访问最后一个元素,避免硬编码


网站公告

今日签到

点亮在社区的每一天
去签到