先记住这个答案
切片是包含指针、长度和容量的描述符。len给出实际元素个数,索引必须小于它;cap表示切片可从其首元素延伸到所属底层数组结尾的元素总数。进行a[low:high]这样的reslice时,high必须≤cap,否则panic;append时若len+新增数≤cap,则直接修改底层数组,否则Go分配更大数组并复制原元素,返回新切片,原切片仍指向旧数组。因此,len控制访问边界,cap控制内存重用边界。
- len是现有元素数,cap是可扩展上限。
- reslice的高索引不能超过cap。
- append超过cap时分配新底层数组。
切片头三字段与索引范围
Go的切片在运行时是一个三字段结构:指向底层数组的指针、长度len和容量cap。len是切片中实际存储的元素数量,它是for range和索引操作的上界。调用cap则得到从该切片第一个元素起,底层数组中仍可连续使用的元素总数,该值通常不小于len。例如make([]int, 3, 5)会创建len=3, cap=5的切片,底层数组总长为5。
当代码执行s[i]时,运行时要求i >= 0 && i < len(s),否则抛出index out of range。而reslice表达式s[low:high]的约束是0 ≤ low ≤ high ≤ cap(s)——注意high的上限是cap而不是len,所以可以用s[:5]将上面例子的长度扩展到5,此时新增元素是底层数组中已有的内容。若写成s[:6],则因cap=5直接触发panic。
Read循环:cap限制单次读取上限,len记录已读数据
假设要从网络连接读取消息,预先分配一个容量为1024字节的缓冲区:buf := make([]byte, 0, 1024),此时len=0, cap=1024。每次循环先构造临时切片chunk := buf[len(buf):cap(buf)],其长度正好等于剩余可用空间cap-len。然后调用n, _ := conn.Read(chunk),由于chunk的长度就是可写入上限,Read最多写入这么多字节。随后执行buf = buf[:len(buf)+n],把本次读入的n字节并入buf,更新len而cap不变。
这种写法巧妙地同时利用了len和cap:cap提供了单次可读取的最大长度,len维护了当前有效数据边界。当len增长到cap(此处为1024)时,chunk的长度变为0,Read将不会写入任何数据。此时若消息尚未读完,就必须用append或重新申请更大缓冲区。这里的关键是,reslice时把high设为cap(buf)是合法的,因为它没有突破cap,否则代码会在运行时panic。
len==cap时的扩展困境与错误直觉
当切片的len等于cap时,表示底层数组剩余可用空间为零。此时任何试图通过reslice扩大长度的操作都会因为high>cap而panic,因为cap是硬上限。例如a := make([]int, 3, 3),执行a[:4]必panic,而a[:3]合法但长度不变。若继续追加数据,只能调用append(a, x),它会自动分配一个新底层数组(容量通常翻倍),原切片继续指向旧数组,之后对新切片赋值不会影响旧切片。
容易犯的错误是认为只要append没有超过cap就必然修改原切片。准确条件是:len + 所需增加数 ≤ cap时,append才会复用原数组,否则必然分配新数组。另一个误区是当len接近cap时,有人喜欢用if len(s) < cap(s) { s = s[:len(s)+1] }来手动扩容。这样虽能增加len,但未对新位置写入有价值的数据(可能留下零值),且若不检查cap会直接panic。更安全的做法是用append并接收返回值。
容易答错的地方
- 认为append超过cap后原切片自动更新
- 许多初学者以为
append总会原位追加,但超过cap后append会分配新底层数组并拷贝,仅返回的新切片指向新数组,旧切片仍指向原数组,二者数据独立。因此必须用slice = append(slice, x)的形式接收返回值。 - 误解reslice时high不能超过len
- 误解reslice时high不能超过len
切片表达式的
high允许超过len,只要不超过cap。例如make([]int,3,5)后,a[:5]合法,得到新切片长度5,此时可以安全访问索引3、4。但在执行a[:5]之前,直接索引a[3]或a[4]会因当前len为3而越界;执行后则不再越界。
面试官还会怎么问?
append超过cap后新切片的容量如何增长?
Go规范不保证具体倍数。常见实现中,原容量小于1024时按2倍增长,大于等于1024时约按1.25倍增长,但不同版本可能调整。编程时不应依赖精确倍数,只依赖cap在新长度增加后足够容纳原有元素和新追加元素。
对nil切片执行reslice会panic吗?
不一定会panic。nil切片的len和cap均为0,切片表达式s[low:high]必须满足0 ≤ low ≤ high ≤ cap。例如s[0:0]合法,得到空切片;而任何low>0或high>0的表达式(如s[0:1]、s[1:])都会因越界而panic。nil切片可以调用append,它会自动分配底层数组并返回可用的新切片。判断切片是否为空应使用len(s)==0,而非s==nil。
如何安全地检查能否用reslice扩展而不越界?
扩展前先检查目标长度n是否满足n <= cap(s),例如if n <= cap(s) { newS := s[:n] }。扩展后新长度可以大于旧len,但新增部分必须位于底层数组有效范围内。如果还要写入具体值,可能触发扩容,更安全的做法是使用append并接受它返回的新切片。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。