【golang】slice与array区别

1

在 Go 里,**数组(array)切片(slice)**看起来很像,但本质上完全不是一回事,这是很多人一开始容易混淆的点。
我按几个维度给你梳理一下:


1. 定义方式 & 是否定长

// 数组:长度是类型的一部分,固定长度
var a1 [3]int          // 长度为 3 的 int 数组
a2 := [3]int{1, 2, 3}  // 字面量

// 切片:动态长度,本质是对底层数组的一层“视图”
var s1 []int           // nil 切片
s2 := []int{1, 2, 3}   // 字面量
s3 := make([]int, 3)   // 长度为 3,容量为 3 的切片
  • 数组:[3]int[4]int两种不同的类型

  • 切片:[]int 只有一种类型,长度可变。


2. 值语义 vs 引用语义(传参时的差异)

数组是值类型
传给函数会被 拷贝 一份,函数内部改的是副本。

func modifyArray(a [3]int) {
    a[0] = 100
}

func main() {
    arr := [3]int{1, 2, 3}
    modifyArray(arr)
    fmt.Println(arr) // [1 2 3] 不变
}

切片是引用类型(更准确地说:包含对底层数组的引用)
传参时拷贝的是一个结构体,但这个结构体内部持有底层数组的指针,所以对元素的修改会反映到外面。

func modifySlice(s []int) {
    s[0] = 100
}

func main() {
    s := []int{1, 2, 3}
    modifySlice(s)
    fmt.Println(s) // [100 2 3] 被修改
}

总结一下:

  • 数组传参:不会影响原数据(除非用指针 *[3]int)。

  • 切片传参:一般会影响原数据(因为指向同一个底层数组)。


3. 内部结构:数组是数据本体,切片是“描述符”

可以简单理解为:

  • 数组:就是一块连续内存,自己就是数据本体

  • 切片:是一个结构体,通常包含三部分:

    • 指向底层数组的指针

    • 长度(len)

    • 容量(cap)

切片的定义类似于:

type sliceHeader struct {
    Data uintptr // 指向底层数组
    Len  int
    Cap  int
}

因此:

  • 切片只是“视图”,多个切片可以共享同一个底层数组。

  • 对切片元素的修改,实际是在改底层数组。


4. 长度和容量

数组

  • len(arr):数组长度

  • 没有 cap 概念,因为数组大小固定。

切片

  • len(s):当前使用的长度

  • cap(s):底层数组的容量(最多能放多少元素,未重新分配前)

s := make([]int, 2, 5)
fmt.Println(len(s)) // 2
fmt.Println(cap(s)) // 5

5. 扩容行为(只存在于切片)

数组长度固定,不能扩容。

切片可以 append,如果容量不够,Go 会:

  1. 分配一个更大的底层数组

  2. 拷贝原有数据

  3. 返回新的切片(指向新的数组)

注意:扩容后新旧切片不再共享底层数组

s := []int{1, 2, 3}
s2 := s          // s2 和 s 共用底层数组
s = append(s, 4) // 可能触发扩容

s[0] = 100
fmt.Println(s)  // [100 2 3 4]
fmt.Println(s2) // 如果扩容了,这里还是 [1 2 3]

所以:

  • 在容量足够时,append 后新旧切片还是共享底层数组。

  • 在触发扩容后,新切片使用新的底层数组,旧切片继续指向旧的。


6. 在函数签名和 API 设计上的使用场景

什么时候用数组?

  • 固定长度并且长度是类型的一部分有意义,比如:

    • IPv4 地址 [4]byte

    • MD5 哈希值 [16]byte

  • 多见于底层库、协议实现、性能要求极高的场景。

更多时候用切片:

  • 绝大多数业务代码都用切片:

    • 动态列表

    • 集合

    • 返回多个值

  • 标准库很多接口也是 []byte[]T 而不是 [N]T


7. 作为 map 的 key

  • 数组可以作为 map 的 key(因为是值类型,支持比较):

m := make(map[[2]int]string)
m[[2]int{1, 2}] = "point"
  • 切片不能作为 map 的 key(切片不可比较):

m := make(map[[]int]string) // 编译错误:invalid map key type []int

8. nil 切片 vs 空切片

var s1 []int        // nil 切片
s2 := []int{}       // 空切片
s3 := make([]int,0) // 空切片
  • s1 == niltrue

  • s2s3 不为 nil,但 len 都是 0

在原理上:

  • nil 切片Data = 0Len = 0Cap = 0

  • 空切片Data 指向某个合法地址(一般是一个零长数组),Len = 0Cap >= 0

使用上:

  • 大多数场景下区别不大

  • 有些 JSON 序列化 / 数据库框架对 nil[] 的处理会不一样

9. 小结一句话版

  • 数组:

    • 固定长度,长度是类型的一部分

    • 值类型,传参会拷贝

    • 本身就是底层数据

  • 切片:

    • 动态长度,灵活,日常开发基本都用它

    • 引用到底层数组,对元素的修改会影响共享同一底层数组的其它切片

    • 可以自动扩容(但可能导致底层数组更换)

  • 切片本质是一个 (指针 + 长度 + 容量) 的结构,指向一个底层数组。

  • 多个切片可以 共享同一个底层数组

  • append 在容量足够时复用原数组;容量不够时会 分配新数组并拷贝数据

  • 切片变量传参/赋值本身是值拷贝,但因为里面有“指针”,所以表现出“引用语义”。