循环里 s += x 会很慢是常识,但慢多少、Builder 和 Buffer、Join 又有多大差别?这篇把几种拼接方式在 100 到 5 万段上各量一遍,并看 String() 是否复制、容量怎么增长、为什么不能复制 Builder。
拼接 100 段(每段 16 字节,结果 1600 字节):s += piece 10717 ns/次 84848 B/次 99 次分配
拼接 100 段(每段 16 字节,结果 1600 字节):strings.Builder 777 ns/次 5360 B/次 9 次分配
拼接 100 段(每段 16 字节,结果 1600 字节):Builder + Grow 348 ns/次 1792 B/次 1 次分配
拼接 100 段(每段 16 字节,结果 1600 字节):bytes.Buffer 985 ns/次 5824 B/次 7 次分配
拼接 100 段(每段 16 字节,结果 1600 字节):strings.Join 1074 ns/次 3584 B/次 2 次分配
拼接 100 段(每段 16 字节,结果 1600 字节):Sprintf("%s%s") 15218 ns/次 86518 B/次 199 次分配
拼接 1000 段(每段 16 字节,结果 16000 字节):s += piece 1088907 ns/次 8470466 B/次 999 次分配
拼接 1000 段(每段 16 字节,结果 16000 字节):strings.Builder 9330 ns/次 62960 B/次 16 次分配
拼接 1000 段(每段 16 字节,结果 16000 字节):Builder + Grow 3050 ns/次 16384 B/次 1 次分配
拼接 1000 段(每段 16 字节,结果 16000 字节):bytes.Buffer 7138 ns/次 49088 B/次 10 次分配
拼接 1000 段(每段 16 字节,结果 16000 字节):strings.Join 12237 ns/次 32768 B/次 2 次分配
拼接 1000 段(每段 16 字节,结果 16000 字节):Sprintf("%s%s") 1023500 ns/次 8509957 B/次 2007 次分配
拼接 10000 段(每段 16 字节,结果 160000 字节):s += piece 60250200 ns/次 834891065 B/次 10048 次分配
拼接 10000 段(每段 16 字节,结果 160000 字节):strings.Builder 72262 ns/次 686834 B/次 24 次分配
拼接 10000 段(每段 16 字节,结果 160000 字节):Builder + Grow 28452 ns/次 163841 B/次 1 次分配
拼接 10000 段(每段 16 字节,结果 160000 字节):bytes.Buffer 73393 ns/次 688066 B/次 14 次分配
拼接 10000 段(每段 16 字节,结果 160000 字节):strings.Join 104039 ns/次 327680 B/次 2 次分配
拼接 10000 段(每段 16 字节,结果 160000 字节):Sprintf("%s%s") 122835412 ns/次 1528501646 B/次 27124 次分配
拼接 50000 段(每段 16 字节,结果 800000 字节):s += piece 1446626958 ns/次 20198789680 B/次 51011 次分配
拼接 50000 段(每段 16 字节,结果 800000 字节):strings.Builder 493997 ns/次 4127530 B/次 31 次分配
拼接 50000 段(每段 16 字节,结果 800000 字节):Builder + Grow 170837 ns/次 802823 B/次 1 次分配
拼接 50000 段(每段 16 字节,结果 800000 字节):bytes.Buffer 359572 ns/次 2899929 B/次 16 次分配
拼接 50000 段(每段 16 字节,结果 800000 字节):strings.Join 540718 ns/次 1605645 B/次 2 次分配段数每增加 10 倍,s += 的耗时增加约 50–70 倍(10717 ns → 1.09 ms → 60 ms → 1.45 s),分配的内存更夸张:5 万段累计分配 20.2 GB,因为每次都把整个字符串复制一遍。Sprintf("%s%s") 更慢。
strings.Builder.String():每次 0 次分配;bytes.Buffer.String():每次 1 次分配(复制了一份)
strings.Builder 在 100、1000、1 万、5 万段下分别是 777 ns、9.3 µs、72 µs、494 µs,每段数量级×10,耗时约×8;如果知道大小就 Grow,只有 1 次分配,再快 2–3 倍。bytes.Buffer 与 Builder 同一量级,strings.Join 在这个测试里略慢(它还要先构造 parts 切片)。
Builder 连续写 2000 个 16 字节段(共 32000 字节),容量变化依次为:16 → 32 → 64 → 128 → 256 → 512 → 896 → 1408 → 2048 → 3072 → 4096 → 5376 → 6912 → 9472 → 12288 → 16384 → 21760 → 28672 → 40960
Builder.String() 不复制(0 次分配),bytes.Buffer.String() 每次复制一份。Builder 的容量起步按 2 倍增长,超过约 512 字节后增长因子变小,32000 字节最终是 40960 的容量。
复制一个已写入的 Builder 后再写:panic: strings: illegal use of non-zero Builder copied by value
Builder 记着自己的地址,复制一个已经写过的再继续写会 panic。把它放在结构体里时,传指针。
// Has unexported fields. } A Builder is used to efficiently build a string using Builder.Write methods. It minimizes memory copying. The zero value is ready to use. Do not copy a non-zero Builder.
文档只用几句话概括:用 Write 方法高效构造字符串,最小化复制,零值可用,不要复制非零值的 Builder。