Java 开发者初学 Go,最容易在这里“翻车”:
- array 和 slice 看起来都像 List
- slice 传参怎么会改到原数据?
- append 到底会不会影响原切片?
- 为什么 Go 还保留数组这种东西?
这篇一次把 数组 vs 切片的本质、使用方式和坑讲清楚。
一、一句话结论(先记住)
数组是值类型,切片是“指向数组的视图”。
Go 几乎只用切片,数组主要用于类型约束和底层实现。
二、数组(array):值类型,很“硬”
定义数组
var a [3]intb := [3]int{1, 2, 3}- 长度是 类型的一部分
- [3]int 和 [4]int 是 不同类型
func f(arr [3]int) {}func g(arr [4]int) {}f(b) // okg(b) // ❌数组是值拷贝(重点)
a := [3]int{1, 2, 3}b := ab[0] = 100fmt.Println(a) // [1 2 3]fmt.Println(b) // [100 2 3] 完全拷贝
行为更接近 Java 的 int[] 被 Arrays.copyOf
数组的使用场景(很少)
- 固定长度(如协议、位图)
- 作为 map key
- 性能极端敏感场景
三、切片(slice):Go 的灵魂
定义切片
var s []int // nil slices1 := []int{1, 2, 3}s2 := make([]int, 3) // [0 0 0]对 Java 开发者来说:
slice ≈ List + 指针语义
slice 的内部结构(非常重要)
type slice struct { ptr *T // 指向底层数组 len int cap int}slice 不是数组,而是 描述数组的一段
四、切片是“引用语义”(高频坑)
func modify(s []int) { s[0] = 100}arr := []int{1, 2, 3}modify(arr)fmt.Println(arr) // [100 2 3]这和 Java List 非常像。
五、切片截取(共享底层数组)
s := []int{1, 2, 3, 4, 5}a := s[1:3] // [2 3]b := s[2:4] // [3 4]a[0] = 200fmt.Println(s) // [1 200 3 4 5]⚠️ 多个切片可能共享同一块内存
六、append 的真相(面试高频)
情况 1:容量足够(共用数组)
s := make([]int, 0, 5)s = append(s, 1)不会分配新数组
情况 2:容量不够(触发扩容)
s := []int{1, 2, 3}t := append(s, 4)- 可能分配新数组
- t 和 s 可能不再共享内存
⚠️ 所以:
func add(s []int) { s = append(s, 10)}不会影响外部 slice 长度
七、copy:显式拷贝
dst := make([]int, len(src))copy(dst, src)这是你“断开引用”的唯一正确方式。
八、nil slice vs 空 slice(细节)
var s1 []int // nils2 := []int{} // empty对比 | nil slice | 空 slice |
len | 0 | 0 |
cap | 0 | 0 |
== nil | true | false |
append | ok | ok |
序列化 / JSON 时区别很大。
九、for-range 遍历切片的坑(经典)
for _, v := range s { v = 100}❌ 不会修改原切片
正确写法:
for i := range s { s[i] = 100}十、Java vs Go 对照总结
维度 | Java List | Go slice |
是否拷贝 | 引用 | 引用 |
扩容 | 自动 | 自动 |
线程安全 | 可选 | 不安全 |
底层可见 | 不可见 | 可感知(len/cap) |
十一、一句话总结
数组是“数据本体”,切片是“操作视图”。
在 Go 里:能用 slice,就不要用 array。
