一次性带你彻底学透 go 的 len 和 cap 的用法
len 和 cap 看似简单,实则藏着 Go 对内存优化的核心逻辑,尤其是切片的扩容机制,更是面试和开发中的高频考点。
本篇文章就带大家从基础到实战,彻底吃透它们的用法。
一、关于 len 和 cap
在 Go 里,len 和 cap 是两个内置函数,都用来描述 “集合类型” 的某种 “长度”,但定义完全不同,先记住核心区别:
- len():获取集合中“当前实际存在的元素个数”——不管容器多大,只看装了多少东西。
- cap():获取集合的“最大容量”——在不重新分配内存的前提下,容器最多能装多少元素(仅对数组、切片、通道有效,map 没有 cap概念)。
不同集合类型中,len 和 cap 的表现差异很大,这也是最容易踩坑的地方
二、len 和 cap 的具体用法
Go 中支持 len/cap 的集合主要有4类:数组、切片、map、通道。
其中切片的用法最复杂,也是重点;map 和通道的用法相对固定。
(一)数组
len 和 cap 永远相等,且不可变
数组是“固定长度的序列”,定义时就确定了长度,一旦创建就不能修改。
所以数组的 len 和 cap 永远等于定义时的长度,完全一致。
package main
import "fmt"
func main() {
// 定义长度为5的int数组
var arr1 [5]int
fmt.Printf("arr1: len=%d, cap=%d\n", len(arr1), cap(arr1)) // 输出:arr1: len=5, cap=5
// 初始化部分元素的数组
arr2 := [5]int{1, 2, 3}
fmt.Printf("arr2: len=%d, cap=%d\n", len(arr2), cap(arr2)) // 输出:arr2: len=5, cap=5
// 未初始化的元素默认是0,len仍为5(实际元素个数是5,不是3)
fmt.Println("arr2[3] =", arr2[3]) // 输出:arr2[3] = 0
// 数组切片化后,len和cap会变化(后续讲切片)
slice := arr2[1:3]
fmt.Printf("slice: len=%d, cap=%d\n", len(slice), cap(slice)) // 输出:slice: len=2, cap=4
}数组的 len 和 cap 是“天生固定”的,等于定义时的长度,不受元素是否初始化影响。
即使只给前 3 个元素赋值,剩下的 2 个元素默认是零值,也算作“实际存在的元素”,所以 len 还是 5。
(二)切片
len 和 cap 的核心用法,关乎内存优化
切片是“动态数组”,底层依赖数组实现。
它的 len 是当前元素个数,cap 是底层数组的可用长度(从切片的起始索引到底层数组末尾的长度)。
切片的 cap 是优化内存的关键——避免频繁分配内存。
切片的 len 和 cap 初始化规则
创建切片有两种常见方式:
- 基于数组切片化
- 用 make 函数创建
两种方式的 len 和 cap 计算规则不同
package main
import "fmt"
func main() {
// 1. 基于数组切片化:slice = arr[start:end]
var arr [5]int{1,2,3,4,5}
// 从索引1切到索引3(左闭右开,实际元素是arr[1], arr[2])
slice1 := arr[1:3]
// len=end-start=2,cap=数组长度 - start=5-1=4
fmt.Printf("slice1: len=%d, cap=%d\n", len(slice1), cap(slice1)) // 输出:slice1: len=2, cap=4
// 2. 用make创建:make([]T, len, cap),cap可选(默认等于len)
// 方式1:只指定len,cap=len
slice2 := make([]int, 3)
fmt.Printf("slice2: len=%d, cap=%d\n", len(slice2), cap(slice2)) // 输出:slice2: len=3, cap=3
// 方式2:同时指定len和cap(cap必须≥len)
slice3 := make([]int, 3, 5)
fmt.Printf("slice3: len=%d, cap=%d\n", len(slice3), cap(slice3)) // 输出:slice3: len=3, cap=5
// 此时切片有3个零值元素,还能再装2个元素而不扩容
}append 操作对 len 和 cap 的影响
切片的 len 会随着 append 元素而增加,但 cap 只有当“元素个数超过当前 cap ”时才会触发“扩容”——分配新的底层数组,把原数据拷贝过去,此时 cap 会翻倍(或按比例增长)
package main
import "fmt"
func main() {
// 初始化len=3,cap=5的切片
s := make([]int, 3, 5)
fmt.Printf("初始: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // 初始: len=3, cap=5, 元素=[0 0 0]
// append 1个元素:未超过cap,len+1,cap不变
s = append(s, 4)
fmt.Printf("append 1个: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // append 1个: len=4, cap=5, 元素=[0 0 0 4]
// 再append 2个元素:超过cap(4+2=6>5),触发扩容
s = append(s, 5, 6)
fmt.Printf("append 2个: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // append 2个: len=6, cap=10, 元素=[0 0 0 4 5 6]
// 再append 5个元素:超过cap=10,再次扩容
s = append(s, 7,8,9,10,11)
fmt.Printf("append 5个: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // append 5个: len=11, cap=20, 元素=[0 0 0 4 5 6 7 8 9 10 11]
}切片扩容的核心规则(Go 1.18+)
很多人以为切片扩容就是“ cap 翻倍”,其实不严谨,Go 1.18 之后的扩容规则更精细,分两种情况:
- 当切片的当前 cap <= 1024 时,扩容后 cap= 当前 cap×2(翻倍);
- 当切片的当前 cap > 1024 时,扩容后 cap= 当前 cap×1.25(增加25%);
这个规则是为了平衡“内存分配效率”和“内存浪费”——小切片翻倍扩容效率高,大切片按比例扩容避免浪费过多内存。
关于 go 切片的源码剖析可以移步文章:Go 源码深度解析之切片 Slice
(三) map
只有 len,没有 cap
map 是“键值对集合”,它没有 cap 的概念,只有 len——表示当前map中实际存在的键值对个数。
有些人会把 map 初始化时的“预分配容量”和 cap 混淆,其实那只是给 Go runtime 的“内存提示”,不是 cap。
package main
import "fmt"
func main() {
// 初始化时指定预分配容量(不是cap!)
m1 := make(map[string]int, 10) // 预分配10个键值对的内存
fmt.Printf("m1: len=%d\n", len(m1)) // 输出:m1: len=0(无元素)
// 无法调用cap(m1),会编译报错:invalid argument m1 (type map[string]int) for cap
// 添加3个键值对
m1["a"] = 1
m1["b"] = 2
m1["c"] = 3
fmt.Printf("m1: len=%d\n", len(m1)) // 输出:m1: len=3
// 直接声明map
var m2 map[string]int
fmt.Printf("m2: len=%d\n", len(m2)) // 输出:m2: len=0(未初始化的map是nil,len为0)
}map 没有 cap,初始化时的“预分配容量”只是让 Go 提前分配内存,减少后续添加元素时的“哈希表扩容”开销,不影响 len 的计算。
(四)通道
cap 是缓冲容量,len 是当前缓冲元素数
通道(channel)分为“无缓冲通道”和“有缓冲通道”,len 和 cap 只对有缓冲通道有意义:
- cap:通道的“缓冲容量”——创建时指定的最大缓冲元素个数;
- len:通道中“当前已缓冲的元素个数”(未被读取的元素数);
package main
import "fmt"
func main() {
// 创建缓冲容量为3的有缓冲通道
ch := make(chan int, 3)
fmt.Printf("初始: len=%d, cap=%d\n", len(ch), cap(ch)) // 输出:初始: len=0, cap=3
// 向通道发送2个元素(未超过cap)
ch <- 1
ch <- 2
fmt.Printf("发送2个后: len=%d, cap=%d\n", len(ch), cap(ch)) // 输出:发送2个后: len=2, cap=3
// 读取1个元素
<-ch
fmt.Printf("读取1个后: len=%d, cap=%d\n", len(ch), cap(ch)) // 输出:读取1个后: len=1, cap=3
// 无缓冲通道:cap=0,len永远是0(发送和接收会阻塞,没有缓冲元素)
ch2 := make(chan int)
fmt.Printf("无缓冲通道: len=%d, cap=%d\n", len(ch2), cap(ch2)) // 输出:无缓冲通道: len=0, cap=0
}常见问题
Q1.切片 append 后,原切片的 len 和 cap 会变吗?
用 append 给切片 s1 添加元素后,发现原切片 s1 的 len 没变,新切片 s2 的 len 和 cap 变了,这是为什么?
package main
import "fmt"
func main() {
s1 := make([]int, 3, 5)
s2 := append(s1, 4)
fmt.Printf("s1: len=%d, cap=%d\n", len(s1), cap(s1)) // 输出:s1: len=3, cap=5
fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2)) // 输出:s2: len=4, cap=5
}原因:append 函数不会修改原切片,而是返回一个“新的切片”。
如果 append 未触发扩容,新切片和原切片共享底层数组;如果触发扩容,新切片会指向新的底层数组。
解决:永远用“原切片 = append (原切片, 元素)”的方式更新切片,确保拿到最新的 len 和 cap:
s1 = append(s1, 4) // 正确:用新切片覆盖原切片
fmt.Printf("s1: len=%d, cap=%d\n", len(s1), cap(s1)) // 输出:s1: len=4, cap=5Q2. 切片作为函数参数时,修改后 len 和 cap 会影响外部吗?
问题:在函数内部给切片 append 元素,外部切片的 len 和 cap 没变化,为什么?
package main
import "fmt"
// 函数内给切片添加元素
func addElem(s []int) {
s = append(s, 100)
fmt.Printf("函数内: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // 函数内: len=4, cap=5, 元素=[0 0 0 100]
}
func main() {
s := make([]int, 3, 5)
fmt.Printf("调用前: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // 调用前: len=3, cap=5, 元素=[0 0 0]
addElem(s)
fmt.Printf("调用后: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // 调用后: len=3, cap=5, 元素=[0 0 0]
}原因:Go 里所有参数都是“值传递”,切片作为参数时,传递的是“切片头的副本”(切片头包含指针、len、cap )。
函数内修改的是副本的len 和 cap,原切片的切片头完全不受影响——这就像你复制了一把钥匙,修改复制的钥匙不会改变原钥匙。
解决:让函数返回修改后的新切片,外部用原切片变量接收,相当于用新钥匙替换原钥匙:
// 改为返回新切片
func addElemReturn(s []int) []int {
s = append(s, 100)
fmt.Printf("函数内: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // 函数内: len=4, cap=5, 元素=[0 0 0 100]
return s
}
func main() {
s := make([]int, 3, 5)
s = addElemReturn(s) // 接收返回的新切片
fmt.Printf("调用后: len=%d, cap=%d, 元素=%v\n", len(s), cap(s), s) // 调用后: len=4, cap=5, 元素=[0 0 0 100]
}Q3. 切片共享底层数组时,修改一个会影响另一个?
问题:基于同一个数组或切片切片化后,得到两个切片,修改其中一个的元素,另一个也跟着变,甚至 cap 也会间接受影响,这是怎么回事?
package main
import "fmt"
func main() {
arr := [5]int{1,2,3,4,5}
s1 := arr[1:3] // len=2, cap=4
s2 := arr[2:4] // len=2, cap=3
fmt.Printf("修改前 s1: %v, s2: %v\n", s1, s2) // 修改前 s1: [2 3], s2: [3 4]
// 修改s1的第二个元素(对应arr[2])
s1[1] = 300
fmt.Printf("修改后 s1: %v, s2: %v\n", s1, s2) // 修改后 s1: [2 300], s2: [300 4]
// s2的第一个元素也变了!因为共享底层数组arr[2]
}原因:基于同一底层数组的切片会“共享内存”,切片的cap取决于底层数组的剩余长度。
修改其中一个切片的元素,本质是修改共享的底层数组内容,自然会影响其他切片;如果其中一个切片触发扩容(分配新数组),则会和其他切片解除共享。
解决:如果需要独立的切片,用copy函数创建新切片,脱离对原底层数组的依赖:
func main() {
arr := [5]int{1,2,3,4,5}
s1 := arr[1:3]
// 用copy创建独立切片:先make一个同len的切片,再复制内容
s2 := make([]int, len(s1))
copy(s2, s1)
s1[1] = 300
fmt.Printf("s1: %v, s2: %v\n", s1, s2) // s1: [2 300], s2: [2 3] (s2不受影响)
}Q4. nil 切片和空切片的 len/cap 有什么区别?
问题:面试常问“nil 切片和空切片一样吗?”,很多人只知道两者都没元素,但不清楚 len/cap 的细节和使用差异。
package main
import "fmt"
func main() {
// 1. nil切片:未初始化的切片
var s1 []int
fmt.Printf("nil切片 s1: len=%d, cap=%d, 是nil? %v\n", len(s1), cap(s1), s1 == nil) // nil切片 s1: len=0, cap=0, 是nil? true
// 2. 空切片:已初始化但无元素
s2 := make([]int, 0)
s3 := []int{}
fmt.Printf("空切片 s2: len=%d, cap=%d, 是nil? %v\n", len(s2), cap(s2), s2 == nil) // 空切片 s2: len=0, cap=0, 是nil? false
fmt.Printf("空切片 s3: len=%d, cap=%d, 是nil? %v\n", len(s3), cap(s3), s3 == nil) // 空切片 s3: len=0, cap=0, 是nil? false
}原因:nil 切片的底层指针为 nil,未指向任何数组;空切片的底层指针指向一个“空数组”(固定地址),但两者的 len 和 cap 都为 0。
Go 的json.Marshal等函数会把nil切片序列化为null,空切片序列化为[],这是关键差异。
解决:根据场景选择:需要返回“未初始化”语义(如错误场景无数据)用 nil 切片;需要返回“已处理但无数据”语义(如查询结果为空)用空切片,避免序列化出现意外的null。
总结
写了这么多,最后再总结一下:
- 切片优先预分配 cap:知道大致元素数量时,用
make([]T, 0, cap)预分配容量,比如循环 append 1000 个元素时,cap 设为 1000,能避免多次扩容和内存拷贝,提升性能。 - 修改切片后必用返回值:无论是 append 还是函数内处理切片,都要接收返回的新切片并覆盖原变量,否则可能丢失修改或遇到“修改无效”的问题。
- 共享底层数组要谨慎:基于数组或切片切片化后,若需独立修改,务必用
copy创建新切片,避免“一个修改全联动”的意外。
其实 len 和 cap 的本质是 Go 对“内存高效利用”的设计体现——cap 管“内存分配”,len 管“实际使用”。
搞懂它们的关系,不仅能写更高效的代码,还能理解 Go 内存管理的核心思路。
如果你还踩过其他关于 len 和 cap 的坑,欢迎在评论区分享~~~
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!