本文是《100天精通Go基础入门》专栏第 05 篇,订阅专栏不迷路。
本文目录
-
今天你会得到什么
-
一个反直觉的事实:Go 的字符串是"只读"的
-
四种拼法,实测给你看
-
反引号:多行字符串的脾气
-
最容易翻车的地方:Trim 了两端,中间没动

今天你会得到什么
今天结束时,你会知道为什么 s += "x" 在循环里是性能杀手,什么时候该换 strings.Builder,以及反引号字符串"不解析转义"到底意味着什么。
更重要的是一个底层认知:Go 的字符串是不可变的。这一个事实能解释今天所有的现象。
一个反直觉的事实:Go 的字符串是"只读"的
你写 s := "abc",再写 s += "d",看起来是往 s 后面追加了一个字符。
实际发生的是:Go 新建了一段内存,把 “abc” 和 “d” 复制进去,再让 s 指向新内存。旧的 “abc” 等着垃圾回收。每一次 += 都是一次完整的"申请 + 拷贝"。
一次没事。但循环一万次,每次都把越来越长的字符串整个拷一遍,总拷贝量是平方级的。这就是 + 在循环里的原罪——不是 + 慢,是不可变逼着它每次都重来。
四种拼法,实测给你看
// 《100天精通Go基础入门》Day 05 · 平头哥 · 技术改变世界,勤奋改变人生
package main
import (
"fmt"
"strings"
"time"
)
func main() {
fmt.Println("=== 一、四种拼法,同一件事 ===")
s1 := "你好" + "," + "Go"
s2 := fmt.Sprintf("%s,%s", "你好", "Go")
s3 := strings.Join([]string{"你好", "Go"}, ",")
var b strings.Builder
b.WriteString("你好")
b.WriteString(",")
b.WriteString("Go")
s4 := b.String()
fmt.Printf("+ 号拼接 : %q\n", s1)
fmt.Printf("Sprintf : %q\n", s2)
fmt.Printf("Join : %q\n", s3)
fmt.Printf("Builder : %q\n", s4)
fmt.Println("结果完全一致,差别只在底下做的事")
fmt.Println()
fmt.Println("=== 二、循环里拼 5 万次,各花多久 ===")
const n = 50000
t0 := time.Now()
var acc string
for i := 0; i < n; i++ {
acc += "x" // 每次都整串拷贝
}
d0 := time.Since(t0)
t1 := time.Now()
var sb strings.Builder
for i := 0; i < n; i++ {
sb.WriteString("x") // 只追加,按需扩容
}
_ = sb.String()
d1 := time.Since(t1)
t2 := time.Now()
parts := make([]string, 0, n)
for i := 0; i < n; i++ {
parts = append(parts, "x")
}
_ = strings.Join(parts, "")
d2 := time.Since(t2)
fmt.Printf("+= 拼接 : %12v\n", d0)
fmt.Printf("Builder : %12v\n", d1)
fmt.Printf("Join : %12v\n", d2)
fmt.Println()
fmt.Println("=== 三、常用工具函数 ===")
fmt.Printf("Trim 去两端空白: %q\n", strings.Trim(" 你好 ", " "))
fmt.Printf("Split 按逗号拆 : %q\n", strings.Split("a,b,c", ","))
fmt.Printf("Contains 包含? : %v\n", strings.Contains("hello", "ell"))
fmt.Printf("Replace 替换 : %q\n", strings.Replace("a-b-c", "-", "+", 2))
fmt.Printf("Repeat 重复 : %q\n", strings.Repeat("=-", 5))
fmt.Printf("ToUpper 转大写 : %q\n", strings.ToUpper("go!"))
}


跑出来的耗时会因机器而异,但比例稳定:循环里 += 比 Builder 慢一到两个数量级。fmt.Sprintf 介于两者之间,它最灵活,但要解析格式串,热路径上别用。
选型就三句:循环内拼接用 Builder;已有一堆现成片段用 Join;少量固定几段用 + 或 Sprintf。
反引号:多行字符串的脾气
Go 的字符串有两种引号,脾气完全不同。
双引号是"会解析"的:"\n" 是换行,"\t" 是制表符。
反引号是"所见即所得":里面的 \n 就是两个字符,一个反斜杠一个 n,什么也不解析。
// 《100天精通Go基础入门》Day 05 · 平头哥 · 技术改变世界,勤奋改变人生
package main
import "fmt"
func main() {
fmt.Println("=== 双引号:转义会被解析 ===")
a := "第一行\n第二行"
fmt.Printf("%q\n", a)
fmt.Println(a)
fmt.Println()
fmt.Println("=== 反引号:所见即所得 ===")
b := `第一行\n第二行
第三行(这行是源码里真实敲的回车)`
fmt.Printf("%q\n", b)
fmt.Println(b)
fmt.Println()
fmt.Println("=== 反引号的日常用途:写 SQL、写 HTML 模板 ===")
sql := `
SELECT name, price
FROM products
WHERE price > ?
ORDER BY price DESC
LIMIT 10`
fmt.Println(sql)
fmt.Println()
fmt.Println("=== 反引号里放不了反引号本身,双引号里可以放转义 ===")
fmt.Println("路径用双引号: \"C:\\Users\\pingtouge\"")
fmt.Println(`路径用反引号: C:\Users\pingtouge ← 不用双反斜杠,爽`)
}


反引号最大的爽点是写 Windows 路径和 SQL:不用把 \ 写成 \\。它最大的限制是里面不能再出现反引号,需要嵌反引号时(比如拼 shell 命令)只能回双引号。
最容易翻车的地方:Trim 了两端,中间没动
这个坑不报错,编译运行全正常,所以比崩溃更阴。看真实输出:
// 《100天精通Go基础入门》Day 05 · 平头哥 · 技术改变世界,勤奋改变人生
package main
import (
"fmt"
"strings"
)
func main() {
s := " 你好 , Go "
cleaned := strings.Trim(s, " ")
fmt.Printf("原文 : %q\n", s)
fmt.Printf("Trim 之后 : %q\n", cleaned)
fmt.Println("↑ 逗号后面的空格还在!Trim 只处理两端")
fmt.Println()
fmt.Println("想清洗所有空白,得这么写:")
fields := strings.Fields(s)
fmt.Printf("Fields 按空白切: %q\n", fields)
fmt.Printf("再 Join 回去 : %q\n", strings.Join(fields, " "))
}


这个坑不报错,编译运行全正常,所以比崩溃更阴——以为“清洗了空白”,其实中间的空格原样进了数据库或 URL。正确姿势看需求:只要去首尾用 Trim;要按空白切开用 Fields(连续空白当一个分隔符,比 Split(s, " ") 稳);要整体压缩就 Fields + Join 两步。
顺带一个真会编译失败的:反引号字符串里放不了反引号本身。
// 《100天精通Go基础入门》Day 05 · 平头哥
package main
import "fmt"
func main() {
cmd := `echo "hello" && ls -la`
fmt.Println(cmd)
fmt.Println(`嵌一个反引号: ` 试试`)
}

报错发生在第 7 行:反引号字符串遇到第一个反引号就结束了,后面那段成了悬空内容,编译器报一堆语法错。想在字符串里放反引号,只能用双引号加转义:""`。
参考资料

338

被折叠的 条评论
为什么被折叠?



