目录

Go 语言切片

切片(slice)是 Go 语言中使用最频繁的复合类型,它是对数组的动态封装,提供了动态长度、灵活的截取和高效的追加能力。日常开发中,几乎所有集合操作都通过切片完成,可以说切片是 Go 编程的核心数据结构。

切片的定义与创建

使用字面量创建

package main

import "fmt"

func main() {
    // 与数组类似,但不指定长度
    s := []int{1, 2, 3, 4, 5}
    fmt.Println(s)          // [1 2 3 4 5]
    fmt.Printf("类型: %T\n", s) // []int
}

使用 make 创建

make([]T, length, capacity) 用于创建指定长度和容量的切片。

package main

import "fmt"

func main() {
    // 创建长度为 5,容量为 5 的切片,元素为零值
    s1 := make([]int, 5)
    fmt.Println(s1) // [0 0 0 0 0]
    fmt.Printf("len=%d, cap=%d\n", len(s1), cap(s1)) // len=5, cap=5

    // 创建长度为 3,容量为 10 的切片
    s2 := make([]int, 3, 10)
    fmt.Printf("len=%d, cap=%d\n", len(s2), cap(s2)) // len=3, cap=10
}

从数组派生

package main

import "fmt"

func main() {
    arr := [6]int{10, 20, 30, 40, 50, 60}

    // 从数组索引 1 到 4(不含 4)创建切片
    s := arr[1:4]
    fmt.Println(s) // [20 30 40]
}

切片 vs 数组的区别

特性 数组 切片
长度 固定,是类型的一部分 动态,可变
类型 [N]T []T
赋值 值拷贝 引用底层数组
比较 可比较(元素可比较时) 不可用 == 比较
函数传参 拷贝整个数组 只拷贝切片头
package main

import "fmt"

func main() {
    // 数组:值类型
    a := [3]int{1, 2, 3}
    b := a
    b[0] = 100
    fmt.Println(a) // [1 2 3] —— 不受影响

    // 切片:引用底层数组
    s1 := []int{1, 2, 3}
    s2 := s1
    s2[0] = 100
    fmt.Println(s1) // [100 2 3] —— 共享底层数组
}

len() 和 cap() 函数

  • len(s) 返回切片的长度(当前元素个数)
  • cap(s) 返回切片的容量(底层数组从切片起始位置到末尾的元素个数)
package main

import "fmt"

func main() {
    s := make([]int, 3, 5)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=3, cap=5

    s = append(s, 1, 2)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=5, cap=5

    // 超出容量会触发扩容
    s = append(s, 3)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=6, cap=10
}

https://img.zhaojq.top/20260729161536301.png
切片底层结构

append() 函数

append() 用于向切片末尾追加元素,返回新的切片。

package main

import "fmt"

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

    // 追加单个元素
    s = append(s, 4)
    fmt.Println(s) // [1 2 3 4]

    // 追加多个元素
    s = append(s, 5, 6, 7)
    fmt.Println(s) // [1 2 3 4 5 6 7]

    // 追加另一个切片(使用 ... 展开)
    s2 := []int{8, 9}
    s = append(s, s2...)
    fmt.Println(s) // [1 2 3 4 5 6 7 8 9]
}

扩容机制

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

  • 容量 < 256 时:新容量 = 旧容量的 2 倍
  • 容量 >= 256 时:新容量 = 旧容量 × 1.25 + 192(渐进式增长)
package main

import "fmt"

func main() {
    s := make([]int, 0)

    for i := 0; i < 10; i++ {
        s = append(s, i)
        fmt.Printf("追加 %d 后: len=%d, cap=%d\n", i, len(s), cap(s))
    }
}

扩容时会分配新的底层数组,并将旧数据拷贝过去。因此,扩容前后的切片指向不同的底层数组。

切片截取

切片截取语法为 s[low:high],含义如下:

  • low:起始索引(包含)
  • high:结束索引(不包含)
  • 结果长度 = high - low
  • 结果容量 = cap(s) - low
package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Printf("原始: len=%d, cap=%d\n", len(s), cap(s))

    // 基本截取
    sub := s[2:5]
    fmt.Println(sub) // [2 3 4]
    fmt.Printf("sub: len=%d, cap=%d\n", len(sub), cap(sub)) // len=3, cap=8

    // 省略 low,从开头截取
    fmt.Println(s[:3]) // [0 1 2]

    // 省略 high,截取到末尾
    fmt.Println(s[7:]) // [7 8 9]

    // 全部省略,复制切片描述符(仍共享底层数组)
    fmt.Println(s[:]) // [0 1 2 3 4 5 6 7 8 9]
}

截取后的切片与原切片共享底层数组,修改其中一个会影响另一个。

copy() 函数

copy(dst, src)src 中的元素复制到 dst,返回实际复制的元素个数。

package main

import "fmt"

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

    n := copy(dst, src)
    fmt.Println(n)   // 5
    fmt.Println(dst) // [1 2 3 4 5]

    // 修改 dst 不影响 src
    dst[0] = 100
    fmt.Println(src) // [1 2 3 4 5] —— 不受影响
}

copy() 常用于创建独立的切片副本,避免共享底层数组带来的副作用:

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}

    // 错误方式:共享底层数组
    s2 := s1
    s2[0] = 100
    fmt.Println(s1) // [100 2 3]

    // 正确方式:使用 copy 创建独立副本
    s3 := make([]int, len(s1))
    copy(s3, s1)
    s3[0] = 200
    fmt.Println(s1) // [100 2 3] —— 不受影响
    fmt.Println(s3) // [200 2 3]
}

切片的底层结构

切片的底层是一个结构体,包含三个字段:

// runtime 中的定义(简化版)
type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int            // 切片长度
    cap   int            // 切片容量
}
  • array:指向底层数组的指针
  • len:当前切片中元素的个数
  • cap:从切片起始位置到底层数组末尾的容量
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    s := []int{10, 20, 30}

    // 通过 unsafe 查看切片内部结构
    ptr := *(*[3]uintptr)(unsafe.Pointer(&s))
    fmt.Printf("array 指针: %x\n", ptr[0])
    fmt.Printf("len: %d\n", ptr[1])
    fmt.Printf("cap: %d\n", ptr[2])
}

切片共享底层数组的陷阱

由于切片共享底层数组,不当操作可能导致难以排查的 bug。

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3, 4, 5}
    s2 := s1[1:3] // [2, 3],与 s1 共享底层数组

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

    // 修改 s2 的元素会影响 s1
    s2[0] = 200
    fmt.Println(s1) // [1 200 3 4 5]

    // 对 s2 追加,如果未超出容量,也会覆盖 s1 的数据
    s2 = append(s2, 99)
    fmt.Println(s1) // [1 200 3 99 5] —— s1[3] 被覆盖!
}

避免陷阱的原则

  • 截取切片后,谨慎对子切片使用 append()
  • 需要独立数据时,使用 copy() 创建副本
  • 使用 append() 时始终接收返回值

性能建议:预分配容量

如果预先知道切片需要的容量,使用 make() 预分配可以避免多次扩容带来的内存分配和数据拷贝。

package main

import "fmt"

func main() {
    // 不推荐:动态扩容,可能触发多次内存分配
    var s1 []int
    for i := 0; i < 1000; i++ {
        s1 = append(s1, i)
    }

    // 推荐:预分配容量,减少扩容次数
    s2 := make([]int, 0, 1000)
    for i := 0; i < 1000; i++ {
        s2 = append(s2, i)
    }

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

在性能敏感的场景下,预分配容量可以显著减少内存分配次数,提升程序性能。

总结

本篇详细讲解了 Go 语言切片的核心知识:切片的创建方式(字面量、make、从数组派生)、切片与数组的区别、len()cap() 的含义、append() 的追加与扩容机制、切片截取语法、copy() 函数、底层结构(pointer、len、cap)以及共享底层数组的陷阱。切片是 Go 中最灵活、最常用的数据结构,掌握其底层原理和最佳实践对编写高效、安全的 Go 程序至关重要。