目录

一次性带你彻底学透 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=5

Q2. 切片作为函数参数时,修改后 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 的坑,欢迎在评论区分享~~~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-len-cap/

备用原文链接: https://blog.fiveyoboy.com/articles/go-len-cap/