一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶

在 Go 里:

  • 字符串 string 是不可变的(你不能在原来的 string 上直接改)

  • 所以 + / += 反复拼接,经常会很慢(反复分配 + 复制)

  • 为了快,Go 常用两种方式:

    1. []byte + append
    2. strings.Builder 拼(写法更像"拼字符串",性能也好)
  • cap / Grow 都是在做同一件事:提前预留容量,减少扩容拷贝

1. 为什么"拼字符串"会出问题?

你想要构造一个输出,比如:

"blue is sky the"

最直觉的写法是:

go 复制代码
ans := ""
ans += "blue"
ans += " "
ans += "is"

看起来很合理对吧?

但 Go 的 string 有个关键特性:

✅ Go 的 string 不可变(immutable)

你一旦创建了一个 string,它里面的内容不能"原地修改"。

所以 ans += "blue" 实际发生的是:

  1. 新开一块内存,长度 = oldLen + newLen
  2. 把旧的 ans 复制进去
  3. 再把 "blue" 复制进去
  4. ans 指向这块新内存

也就是说:每一次 += 都可能要"重新开数组 + 复制一遍旧内容"

2. 这就是为什么 + 会慢:重复拷贝

举个特别直观的例子:

你要拼 3 次:

  • 第 1 次:复制 0 个旧字符
  • 第 2 次:复制 4 个旧字符("blue")
  • 第 3 次:复制 7 个旧字符("blue is")

你会发现:旧内容一直在被反复复制。

拼得越多,复制越多,速度就越慢。

3. 那怎么快?核心思路:用"可变容器"先装起来

既然 string 不可变,那我们先用一个 可变的容器 装字符,最后一次性变成 string。

Go 最常用的可变容器就是:

[]byte(字节数组 / 可变)

你可以对它 append,它会自动增长:

go 复制代码
ans := make([]byte, 0)
ans = append(ans, 'b', 'l', 'u', 'e')

最后:

go 复制代码
return string(ans)

这样就避免了每次 += 的"重新分配 + 复制旧内容"。

4. 这就引出了:lencap 是啥?

[]byte / []int 这些 slice,在 Go 里有两个重要概念:

  • len:现在已经用了多少
  • cap:底层总共预留了多少空间(还能装多少)

比如:

go 复制代码
ans := make([]byte, 0, 10)

意思是:

  • len = 0(现在里面没东西)
  • cap = 10(底层数组先预留了 10 个位置)

为什么要 cap?

因为如果你不预留,append 可能会这样:

  • 空间不够 → 申请更大数组
  • 把旧数组内容复制过去
  • 才能继续 append

所以:

cap 就是为了减少"扩容 + 拷贝"的次数。

5. 超过 cap 会发生什么?

当你 append 让 len > cap

  • Go 会创建一个更大的新数组
  • 把旧内容复制到新数组
  • slice 指向新数组

你看:又出现"复制旧内容"了对吧?

所以我们才要 尽量提前预留 cap

6. 这时候 strings.Builder 登场:更"像拼字符串"的工具

你用 []byte 拼字符串没问题,但写起来像在操作数组。 strings.Builder 是 Go 官方提供的:

专门用来高效拼接字符串的工具

你可以把它理解成:

"官方封装好的 []byte + append"

它的用法是:

go 复制代码
import "strings"

var b strings.Builder
b.WriteString("blue")
b.WriteByte(' ')
b.WriteString("sky")
result := b.String()

你可以把它理解成:

"内部帮你维护了一个 []byte 的拼接器"

WriteString/WriteByte,它内部就在 append 到那个 buffer 里。

最后 String() 一次性输出。

7. 那 Grow 又是什么?它和 cap 的关系是什么?

现在关键来了:

Builder 内部其实也需要容量,否则也会扩容 + 拷贝。

所以它也需要"预留空间"。

这就是:

b.Grow(n)

意思是:

提前保证:接下来还能再写 n 个字节,不用扩容

对应到 slice 的感觉就是:

  • slice:make([]byte, 0, n) 预留 cap
  • builder:b.Grow(n) 预留内部 buffer 的 cap

所以它们关系是:

Grow 本质上是在给 Builder 内部的"隐藏 slice"扩 cap

9. 小白怎么选?

你现在阶段记住这个就够了:

✅ 简单场景(拼得不多)

+ 也行(比如 2~3 次拼接)

✅ 循环里大量拼接(比如这题、构造大字符串)

优先用:

  • strings.Builder(推荐,易读)
  • []byte + append(你已经在用)

✅ 追求性能时加一条:预留容量

  • ans := make([]byte, 0, len(s))
  • b.Grow(len(s))
相关推荐
程序员布吉岛1 小时前
写了 10 年 MyBatis,一直以为“去 XML”=写注解,直到看到了这个项目
后端
茶杯梦轩1 小时前
从零起步学习Redis || 第七章:Redis持久化方案的实现及底层原理解析(RDB快照与AOF日志)
redis·后端
QZQ541881 小时前
重构即时IM项目13:优化消息通路(下)
后端
柠檬味拥抱1 小时前
揭秘Cookie操纵:深入解析模拟登录与维持会话技巧
后端
不想打工的码农1 小时前
MyBatis-Plus多数据源实战:被DBA追着改配置后,我肝出这份避坑指南(附动态切换源码)
java·后端
ZeroTaboo1 小时前
rmx:给 Windows 换一个能用的删除
前端·后端
Coder_Boy_2 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例
java·人工智能·spring boot·后端·spring
Victory_orsh2 小时前
AI雇佣人类,智能奴役肉体
后端
金牌归来发现妻女流落街头2 小时前
【Springboot基础开发】
java·spring boot·后端