在 Go 语言开发中,len 和 cap 是两组非常基础且常用的内置函数。它们分别用于读取对象的当前长度和可用容量,帮助开发者判断集合是否还能继续追加元素、缓冲区是否已满、切片是否需要进行扩容等。理解这两个函数的适用类型、返回值差异以及底层结构,是写出稳定 Go 代码的重要基础。

len 与 cap 的基本含义和适用边界
len 是 length 的缩写,表示某个对象当前包含的元素个数或字节长度。对于数组、切片、字符串、映射和通道,len 都能给出一个明确的数值。数组返回声明长度,切片返回当前有效元素数量,字符串返回字节长度,映射返回键值对数量,通道返回缓冲区内当前已存放的元素数量。由于 len 只关心“现在有多少”,因此它在循环边界、判空、缓冲区水位检查等场景中非常常见。
cap 是 capacity 的缩写,表示某个对象最多可以容纳多少个元素。与 len 相比,cap 的适用范围更窄,通常只用于数组、切片和通道。数组的容量就是其固定长度,切片的容量表示底层数组从切片起始位置到末尾还能提供多少空间,通道的容量表示创建时指定的缓冲大小。字符串和映射没有对外暴露的容量上限概念,因此对它们调用 cap 会导致编译错误,而不是在运行时返回 0。
适用类型对比
从类型角度看,len 覆盖的范围更广,而 cap 只针对具有明确容量语义的类型。数组因为长度固定,所以 len 和 cap 永远相等;切片因为长度可变,所以两者经常不同;通道因为存在缓冲区,所以 cap 表示缓冲区最大可存放数量,len 表示当前已存放数量。字符串虽然可以计算长度,但没有容量字段;映射是哈希表结构,其内部存储结构会动态调整,但不向外部提供容量查询接口。
package main
import "fmt"
func main() {
// len 适用于字符串和映射
s := "golang"
m := map[string]int{"a": 1, "b": 2}
fmt.Println("字符串 len:", len(s)) // 输出 6
fmt.Println("映射 len:", len(m)) // 输出 2
// 下面两行如果取消注释会导致编译错误
// fmt.Println("字符串 cap:", cap(s))
// fmt.Println("映射 cap:", cap(m))
}
数组、切片与通道中的表现差异
数组是 Go 语言中固定大小的连续存储结构。声明数组时必须指定长度,例如长度为 5 的整型数组,在程序运行期间这个长度不会改变。因此,对数组调用 len 和 cap 的结果必然相同,都等于声明时确定的长度。数组的这种确定性让它适合用于结构体字段、固定数量配置项等场景,但也意味着不能直接通过 append 增加元素。
切片则不同。切片是一个具有引用语义的类型,它本身保存了指向底层数组的指针、当前长度和当前容量。切片的长度可以在运行过程中通过 append、重新切片等操作发生变化,而容量则受到底层数组剩余空间的影响。正因为如此,切片的 len 和 cap 经常不相等。当 len 小于 cap 时,通常还可以在不触发扩容的前提下继续追加元素;当 len 等于 cap 时,再次追加就可能触发底层数组重新分配。
数组示例
package main
import "fmt"
func main() {
// 声明长度为 5 的 int 数组
var arr [5]int
fmt.Println("数组 len:", len(arr)) // 输出 5
fmt.Println("数组 cap:", cap(arr)) // 输出 5
}
切片示例
切片示例中,先创建一个长度为 3、容量为 5 的切片。此时 len 为 3,cap 为 5。继续追加两个元素后,长度变为 5,容量仍为 5,因为底层数组还有足够空间。再追加一个元素时,当前容量已经不够,运行时会创建新的底层数组并复制旧元素,从而产生新的容量。
package main
import "fmt"
func main() {
// 创建长度为 3、容量为 5 的切片
s := make([]int, 3, 5)
fmt.Println("初始 len:", len(s)) // 输出 3
fmt.Println("初始 cap:", cap(s)) // 输出 5
// 追加两个元素,未超过容量
s = append(s, 1, 2)
fmt.Println("追加后 len:", len(s)) // 输出 5
fmt.Println("追加后 cap:", cap(s)) // 输出 5
// 再追加一个元素,超过容量,触发扩容
s = append(s, 3)
fmt.Println("扩容后 len:", len(s)) // 输出 6
fmt.Println("扩容后 cap:", cap(s)) // 输出 10
}
通道示例
通道用于 goroutine 之间通信,其 len 和 cap 分别描述缓冲区当前状态和最大缓冲能力。无缓冲通道的容量为 0,发送和接收必须同步完成;有缓冲通道则允许在一定范围内异步传递数据。通过 len(ch) 可以判断缓冲区中当前有多少未消费的数据,通过 cap(ch) 可以判断缓冲区的最大容量,两者相减即可得到剩余空间。
package main
import "fmt"
func main() {
// 创建缓冲容量为 3 的通道
ch := make(chan int, 3)
fmt.Println("初始 len:", len(ch)) // 输出 0
fmt.Println("初始 cap:", cap(ch)) // 输出 3
// 向通道发送两个元素
ch <- 1
ch <- 2
fmt.Println("发送后 len:", len(ch)) // 输出 2
fmt.Println("发送后 cap:", cap(ch)) // 输出 3
}
切片底层结构与扩容机制
要真正理解切片中 len 和 cap 的区别,需要观察切片的底层结构。从反射接口的角度看,切片头部可以类比为 reflect.SliceHeader,它包含 Data、Len、Cap 三个字段:Data 指向底层数组中切片开始的位置,Len 是当前长度,Cap 是当前容量。Data 决定切片从底层数组的哪个位置开始读取数据;Len 决定 len 返回多少;Cap 决定 cap 返回多少,也决定切片最多可以扩展到多大。由于这些信息都保存在切片头部中,读取 len 和 cap 通常只需要访问对应字段,不需要遍历元素,因此时间复杂度为 O(1)。
切片扩容发生在追加元素超出当前容量时。运行时会根据当前容量、目标长度以及内存分配策略计算新的容量,然后申请新的底层数组,将原有元素复制过去,最后更新切片头部的指针、长度和容量。按照常见的简化规则,当当前容量小于 256 时,新容量通常会翻倍;当当前容量大于等于 256 时,增长速度会放缓,通常可理解为按约 1.25 倍增长。这个机制既保证了小容量切片的快速扩展,也避免了大容量切片一次性分配过多内存。
需要注意的是,扩容会改变底层数组地址。如果多个切片共享同一个底层数组,其中一个切片发生扩容后,新切片指向新的数组,而旧切片仍然指向旧数组。因此,在并发或复杂共享场景下,不能简单假设两个切片一定指向同一块内存。理解这一点,有助于避免“修改一个切片后另一个切片也发生变化”的预期错误。
子切片容量计算
子切片的容量不是从原切片末尾开始计算,而是从子切片起始索引开始,一直计算到底层数组末尾。因此,子切片的容量可能小于原切片,也可能等于原切片,取决于起始位置。理解这一点,可以解释为什么对同一底层数组的不同切片,cap 可能不同。
package main
import "fmt"
func main() {
// 创建长度为 3、容量为 10 的切片
s := make([]int, 3, 10)
// 从索引 1 开始截取到索引 3
subS := s[1:3]
fmt.Println("原切片 cap:", cap(s)) // 输出 10
fmt.Println("子切片 cap:", cap(subS)) // 输出 9
}
常见误区与工程实践建议
第一个常见误区是误以为所有类型都可以使用 cap。实际上,字符串和映射不能使用 cap,否则会编译失败。第二个误区是认为切片的 cap 一定等于 len。对于通过 make 指定容量的切片,或者通过 append 扩容后的切片,cap 往往大于 len;而对于任何有效切片,cap 一定大于等于 len。第三个误区是忽略 nil 切片的行为。对 nil 切片调用 len 和 cap 都会返回 0,并且不会 panic,因此 nil 切片可以作为安全的初始值使用。
在工程实践中,合理使用 len 和 cap 可以提升代码可读性和性能。例如,在循环遍历切片时,使用 len(s) 作为边界,可以避免越界;在批量追加数据前,如果已知最终规模,可以提前使用 make([]int, 0, n) 指定容量,减少多次扩容带来的复制开销;在通道消费逻辑中,使用 len(ch) < cap(ch) 判断缓冲区是否仍有空间,有助于控制生产节奏。需要注意的是,len 和 cap 的返回值在并发场景下可能随时变化,关键逻辑仍应结合通道、互斥锁或原子操作保证一致性。
总结来看,len 回答的是“现在有多少”,cap 回答的是“最多能有多少”。数组中两者相同,切片中两者可能不同,通道中两者分别描述缓冲区的当前状态和最大能力。掌握它们的适用类型、底层字段、扩容规则和常见边界情况,能够帮助开发者更准确地处理集合、缓冲区和并发通信问题,减少因容量误判导致的性能损耗或逻辑错误。