Day 05 _ 字符串拼接_循环里用 + 号,性能掉一半不夸张

本文是《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 行:反引号字符串遇到第一个反引号就结束了,后面那段成了悬空内容,编译器报一堆语法错。想在字符串里放反引号,只能用双引号加转义:""`。

参考资料


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值