第11章:字符串操作——strings 包

第11章:字符串操作——strings 包

“在 Go 的世界里,字符串是 immutable(不可变)的字节序列,听起来很限制对吧?但正是这种设计,让 strings 包成为了处理文本的瑞士军刀——稳定、可靠、从不闹脾气。”


11.1 strings 包解决什么问题:文本处理,查找、替换、分割、拼接、大小写转换

专业解释: strings 包是 Go 标准库中专门用于处理 UTF-8 字符串的工具箱,提供了查找(Contains/Index)、替换(Replace)、分割(Split/Fields)、拼接(Join/Repeat)、大小写转换(ToLower/ToUpper)等核心功能,是日常开发中使用频率最高的包之一。

通俗理解: 想象你有一堆积木(字符串),strings 包就是你的说明书,告诉你怎么找到某块积木、把它换成别的、切成几段、或者把颜色改一改(大小写转换)。

mermaid 概览:

flowchart LR
    A["字符串\nHello World"] --> B[查找]
    A --> C[替换]
    A --> D[分割]
    A --> E[拼接]
    A --> F[大小写]
    B --> B1["Contains Index Count"]
    C --> C1["Replace ReplaceAll"]
    D --> D1["Split Fields SplitN"]
    E --> E1["Join Repeat Builder"]
    F --> F1["ToLower ToUpper Title"]

一句话总结: strings 包让你的字符串操作从"手工作坊"升级为"流水线工厂"。


11.2 strings 核心原理:Go 字符串是不可变的,所有修改操作都返回新字符串

专业解释: 在 Go 中,string 是只读的字节切片(slice of bytes),底层是一个结构体包含指针和长度。所有"修改"操作(如 ToLower、Trim、Replace)实际上是在内存中创建一个新的字符串,原始字符串保持不变。这种设计确保了字符串的线程安全性和可预测性。

通俗理解: 就像你把一张发票复印一份,然后在复印件上涂改——原件还在档案柜里纹丝不动。Go 的字符串就是这么矜持,从不改变自己,只生成分身。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "fmt"
    "strings"
)

func main() {
    original := "Hello, Go!"
    modified := strings.ToLower(original)

    // 打印结果
    fmt.Println("原始字符串:", original)  // 原始字符串: Hello, Go!
    fmt.Println("修改后字符串:", modified) // 修改后字符串: hello, go!
    fmt.Println("原始字符串改变了吗?", original == "Hello, Go!") // 原始字符串改变了吗? true

    // 演示内存地址不同
    fmt.Printf("original 地址: %p\n", &original) // original 地址: 0xc0000...
    fmt.Printf("modified 地址: %p\n", &modified) // modified 地址: 0xc0000...(不同!)
}

图示:

原始字符串: "Hello, Go!"
              ↓
         [不可变] ——复制的过程中——
              ↓
新字符串:   "hello, go!"

为什么这样设计?

  1. 线程安全 —— 多个 goroutine 读取同一个字符串,永远不会出问题
  2. 可预测性 —— 函数不会偷偷改掉你的输入
  3. 内存复用 —— Go 底层会复用相同的子字符串(copy-on-write 优化)

一句话总结: Go 字符串的"不可变性"就像博物馆里的展品——只能看,不能摸,摸就要付钱(创建新字符串)。


11.3 strings.EqualFold:大小写不敏感比较

专业解释: EqualFold 用于比较两个字符串,忽略大小写差异。它按字节逐个比较,并且在比较 UTF-8 字符时会正确处理 Unicode 的大小写映射,返回 bool 值表示是否相等。

通俗理解: 就像比较两个人名是否相同,不管是大写"John"还是小写"john",都认为是同一个人。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本比较
    result1 := strings.EqualFold("Go", "go")       // true
    result2 := strings.EqualFold("Go", "GO")       // true
    result3 := strings.EqualFold("Go", "golang")    // false

    fmt.Println("EqualFold(\"Go\", \"go\"):", result1)    // EqualFold("Go", "go"): true
    fmt.Println("EqualFold(\"Go\", \"GO\"):", result2)    // EqualFold("Go", "GO"): true
    fmt.Println("EqualFold(\"Go\", \"golang\"):", result3) // EqualFold("Go", "golang"): false

    // Unicode 处理
    result4 := strings.EqualFold("中文", "中文")     // true
    result5 := strings.EqualFold("Å", "å")          // true(Unicode 特殊字符)

    fmt.Println("EqualFold(\"中文\", \"中文\"):", result4) // EqualFold("中文", "中文"): true
    fmt.Println("EqualFold(\"Å\", \"å\"):", result5)      // EqualFold("Å", "å"): true

    // 实战场景:忽略大小写的用户名验证
    username := "Admin"
    if strings.EqualFold(username, "admin") {
        fmt.Println("用户名验证通过!") // 用户名验证通过!
    }
}

mermaid 对比:

flowchart LR
    A["\"Go\""] --> B{EqualFold}
    C["\"go\""] --> B
    D["\"GO\""] --> B
    E["\"golang\""] --> B
    B -->|比较结果| R1["true"]
    B -->|比较结果| R2["false"]

一句话总结: EqualFold 就是字符串比较中的"平光镜"——不管大小写,看到的都是同一个东西。


11.4 strings.Compare:按字节比较,返回 -1/0/1

专业解释: Compare 按字节从左到右逐个比较两个字符串,返回 int 值:-1(小于)、0(相等)、1(大于)。它是基于字典序(lexicographical order)的比较,遵循 Go 的字节序规则。

通俗理解: 想象两个学生按学号排队,学号小的站前面。Compare 就是那个报数的人:“你 -1,你 0,你 1”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本比较
    cmp1 := strings.Compare("apple", "banana")  // -1,a < b
    cmp2 := strings.Compare("banana", "apple")  //  1,b > a
    cmp3 := strings.Compare("apple", "apple")   //  0,相等

    fmt.Println("Compare(\"apple\", \"banana\"):", cmp1)  // Compare("apple", "banana"): -1
    fmt.Println("Compare(\"banana\", \"apple\"):", cmp2)  // Compare("banana", "apple"): 1
    fmt.Println("Compare(\"apple\", \"apple\"):", cmp3)   // Compare("apple", "apple"): 0

    // 字节级别比较
    cmp4 := strings.Compare("abc", "abd")       // -1,c < d
    cmp5 := strings.Compare("ABC", "abc")       // -1,大写字母 ASCII 码小于小写

    fmt.Println("Compare(\"abc\", \"abd\"):", cmp4)       // Compare("abc", "abd"): -1
    fmt.Println("Compare(\"ABC\", \"abc\"):", cmp5)       // Compare("ABC", "abc"): -1

    // 对比 EqualFold
    cmp6 := strings.Compare("Go", "go")        // -1,按字节并不相等
    eq7 := strings.EqualFold("Go", "go")       // true,大小写不敏感相等

    fmt.Println("Compare(\"Go\", \"go\"):", cmp6)        // Compare("Go", "go"): -1
    fmt.Println("EqualFold(\"Go\", \"go\"):", eq7)       // EqualFold("Go", "go"): true

    // 实际应用:排序
    fruits := []string{"banana", "Apple", "cherry", "apricot"}
    // 使用 Compare 进行简单排序
    for i := 0; i < len(fruits)-1; i++ {
        for j := i + 1; j < len(fruits); j++ {
            if strings.Compare(fruits[i], fruits[j]) > 0 {
                fruits[i], fruits[j] = fruits[j], fruits[i]
            }
        }
    }
    fmt.Println("排序后:", fruits) // 排序后: [Apple apricot banana cherry]
}

ASCII 码参考:

'A' = 65, 'B' = 66, ... 'Z' = 90
'a' = 97, 'b' = 98, ... 'z' = 122

所以 “ABC” < “abc” 因为 65 < 97。

一句话总结: Compare 就像字典的编排规则——按字母顺序,谁在前谁就"小"。


11.5 strings.Contains:子串包含判断

专业解释: Contains 判断源字符串 s 是否包含子字符串 substr,返回 bool。它内部实现是调用了 Index(s, substr) >= 0,即查找子串位置。

通俗理解: 就像在一锅汤里尝尝有没有放盐——Contains 就是那个告诉你"有"或"没有"的舌头。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    fmt.Println("Contains(\"Hello, World!\", \"World\"):", strings.Contains("Hello, World!", "World"))    // true
    fmt.Println("Contains(\"Hello, World!\", \"world\"):", strings.Contains("Hello, World!", "world"))    // false(大小写敏感)
    fmt.Println("Contains(\"Hello, World!\", \"Go\"):", strings.Contains("Hello, World!", "Go"))          // false

    // 空字符串的特殊情况
    fmt.Println("Contains(\"Hello\", \"\"):", strings.Contains("Hello", ""))   // true(空串被认为在任意位置)
    fmt.Println("Contains(\"\", \"\"):", strings.Contains("", ""))              // true
    fmt.Println("Contains(\"\", \"a\"):", strings.Contains("", "a"))            // false

    // 实战场景:过滤包含关键字的日志
    logs := []string{
        "[INFO] 用户登录成功",
        "[ERROR] 数据库连接失败",
        "[INFO] 发送邮件通知",
        "[WARN] 内存使用率超过 80%",
        "[ERROR] API 请求超时",
    }

    keyword := "ERROR"
    for _, log := range logs {
        if strings.Contains(log, keyword) {
            fmt.Println("错误日志:", log)
        }
    }
    // 错误日志: [ERROR] 数据库连接失败
    // 错误日志: [ERROR] API 请求超时
}

mermaid 示意:

源字符串: "Hello, World!"
子字符串: "World"

查找过程:
"Hello, World!"
      ↓
  找到位置 7
      ↓
返回: true ✓

一句话总结: Contains 就是字符串版的"includes"——问你"里面有没有",回答 yes 或 no。


11.6 strings.ContainsAny:任一字符包含

专业解释: ContainsAny 判断源字符串 s 中是否包含 chars 字符串中的任意一个字符(注意是字符,不是子串)。只要出现 chars 中任意一个 UTF-8 码点,就返回 true。

通俗理解: 就像检查一袋糖果里有没有"草莓味 OR 橘子味 OR 柠檬味"的糖——只要有其中任何一种口味就算过关。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法:chars 中任意一个字符出现即返回 true
    fmt.Println("ContainsAny(\"Hello\", \"ab\"):", strings.ContainsAny("Hello", "ab"))    // false,H/e/l/l/o 都没有 a 或 b
    fmt.Println("ContainsAny(\"Hello\", \"le\"):", strings.ContainsAny("Hello", "le"))    // true,有 l 和 e
    fmt.Println("ContainsAny(\"Hello\", \"xy\"):", strings.ContainsAny("Hello", "xy"))    // false

    // 中文场景
    fmt.Println("ContainsAny(\"你好世界\", \"你 好\"):", strings.ContainsAny("你好世界", "你 好")) // true
    fmt.Println("ContainsAny(\"你好世界\", \"xyz\"):", strings.ContainsAny("你好世界", "xyz"))       // false

    // 空字符串情况
    fmt.Println("ContainsAny(\"Hello\", \"\"):", strings.ContainsAny("Hello", ""))        // false
    fmt.Println("ContainsAny(\"\", \"abc\"):", strings.ContainsAny("", "abc"))              // false

    // 实战:密码强度检查(必须包含特殊字符)
    func checkPassword(password string) bool {
        specialChars := "!@#$%^&*()_+-=[]{}|;:',.<>?/"
        return strings.ContainsAny(password, specialChars)
    }

    passwords := []string{"Password123", "P@ssw0rd!", "admin123", "S3cure!"}
    for _, p := range passwords {
        fmt.Printf("密码 \"%s\" 含特殊字符? %v\n", p, checkPassword(p))
    }
    // 密码 "Password123" 含特殊字符? false
    // 密码 "P@ssw0rd!" 含特殊字符? true
    // 密码 "admin123" 含特殊字符? false
    // 密码 "S3cure!" 含特殊字符? true
}

Contains vs ContainsAny 对比:

函数参数含义匹配方式
Contains(s, substr)子串substr 作为整体,必须全部出现
ContainsAny(s, chars)字符集chars 中任意一个字符出现即可

一句话总结: ContainsAny 是"或"逻辑—— chars 里挑一个出现就赢了。


11.7 strings.ContainsRune:某个字符包含

专业解释: ContainsRune 判断源字符串 s 中是否包含指定的单个 rune(Unicode 码点)。它专门用于检查中文字符或其他非 ASCII 的单个字符。

通俗理解: 就像在通讯录里查一个人——ContainsRune 就是告诉你在不在。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
    "fmt"
    "strings"
)

func main() {
    // ASCII 字符
    fmt.Println("ContainsRune(\"Hello\", 'H'):", strings.ContainsRune("Hello", 'H'))    // true
    fmt.Println("ContainsRune(\"Hello\", 'x'):", strings.ContainsRune("Hello", 'x'))    // false

    // 中文 Unicode
    fmt.Println("ContainsRune(\"你好世界\", '你'):", strings.ContainsRune("你好世界", '你')) // true
    fmt.Println("ContainsRune(\"你好世界\", '中'):", strings.ContainsRune("你好世界", '中')) // false

    // Emoji(也是 rune)
    emoji := "👨‍💻程序员"
    fmt.Println("ContainsRune(\"👨‍💻程序员\", '程'):", strings.ContainsRune(emoji, '程'))     // true
    fmt.Println("ContainsRune(\"👨‍💻程序员\", '👨'):", strings.ContainsRune(emoji, '👨'))       // true(单个码点)

    // rune 的 ASCII 码值
    fmt.Println("ContainsRune(\"ABC\", 65):", strings.ContainsRune("ABC", 65)) // true('A' = 65)

    // 实战:敏感词过滤
    sensitiveWords := []rune{'黄', '赌', '毒', '暴', '恐'}
    content := "这是一段正常的内容"

    hasSensitive := false
    for _, char := range content {
        for _, sensitive := range sensitiveWords {
            if strings.ContainsRune(string(char), sensitive) {
                hasSensitive = true
                break
            }
        }
    }
    fmt.Printf("内容含敏感词? %v\n", hasSensitive) // 内容含敏感词? false
}

mermaid:

字符串: "你好世界"
  ↓ 逐字符检查
['你', '好', '世', '界']
  ↓ ContainsRune('你')
返回: true ✓

一句话总结: ContainsRune 就是"单挑"——只查一个人(字符)在不在,不接受贿赂(多个字符)。


11.8 strings.Count:子串出现次数

专业解释: Count 返回子串 substr 在源字符串 s 中非重叠出现的次数。如果 substr 为空字符串,返回 s 的长度 + 1。

通俗理解: 就像数一篇文章里某个词出现了多少次——Count 就是那个认真负责的校对员。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    fmt.Println("Count(\"cheese\", \"e\"):", strings.Count("cheese", "e"))      // 3
    fmt.Println("Count(\"five\", \"\"):", strings.Count("five", ""))            // 5(特殊规则:返回长度+1)
    fmt.Println("Count(\"\", \"\"):", strings.Count("", ""))                    // 1(空串在空串中出现 1 次)

    // 非重叠计数
    fmt.Println("Count(\"aaaa\", \"aa\"):", strings.Count("aaaa", "aa"))        // 2(非重叠:第一个 aa 和第三个 aa)
    fmt.Println("Count(\"aaa\", \"aa\"):", strings.Count("aaa", "aa"))          // 1(非重叠)

    // 大小写敏感
    fmt.Println("Count(\"GoGoGo\", \"Go\"):", strings.Count("GoGoGo", "Go"))    // 3
    fmt.Println("Count(\"GoGoGo\", \"go\"):", strings.Count("GoGoGo", "go"))    // 0

    // 实战:统计代码行数中的注释
    code := `
    // 这是单行注释
    func main() {
        fmt.Println("Hello") // 行末注释
        /* 这是
           多行注释 */
    }
    `
    singleLineComments := strings.Count(code, "//")
    multiLineComments := strings.Count(code, "/*")
    fmt.Printf("单行注释数量: %d\n", singleLineComments) // 单行注释数量: 2
    fmt.Printf("多行注释开始: %d\n", multiLineComments)  // 多行注释开始: 1

    // 实战:词频统计(简化版)
    text := "go is great, go is fast, go is simple"
    goCount := strings.Count(text, "go")
    fmt.Printf("\"go\" 出现次数: %d\n", goCount) // "go" 出现次数: 3
}

Count 的特殊规则:

1
2
3
4
// 空字符串的特殊返回值
strings.Count("hello", "")   // 返回 6 = len("hello") + 1
strings.Count("", "")        // 返回 1
strings.Count("", "a")       // 返回 0

一句话总结: Count 是字符串界的"点名"——认真地数一数出现了几次,注意是非重叠的哦。


11.9 strings.Fields:按空格分割

专业解释: Fields 将字符串 s 按一个或多个空白字符(空格、Tab、换行等)分割,返回分割后的字符串切片。它会自动去除空白,只保留非空的部分。

通俗理解: 就像把一句话拆成单词——Fields 就是那个把句子切成一个个词语的切菜刀。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    s := "  hello   world  go   "
    fields := strings.Fields(s)

    fmt.Printf("分割结果: %q\n", fields) // 分割结果: ["hello" "world" "go"]

    // 各种空白字符
    s2 := "hello\nworld\tgo\rfoo"
    fields2 := strings.Fields(s2)

    fmt.Printf("含换行符分割: %q\n", fields2) // 含换行符分割: ["hello" "world" "go" "foo"]

    // 连续空白只算一个分隔符
    s3 := "hello    world"
    fields3 := strings.Fields(s3)

    fmt.Printf("连续空白分割: %q\n", fields3) // 连续空白分割: ["hello" "world"]

    // 中英文混合
    s4 := "你好   世界  Go语言"
    fields4 := strings.Fields(s4)

    fmt.Printf("中英混合分割: %q\n", fields4) // 中英混合分割: ["你好" "世界" "Go语言"]

    // 实战:统计单词数
    sentence := "  The quick brown fox jumps over the lazy dog  "
    words := strings.Fields(sentence)
    fmt.Printf("单词数量: %d\n", len(words)) // 单词数量: 9

    // 实战:处理用户输入
    userInput := "  john  doe   "
    parts := strings.Fields(userInput)
    if len(parts) >= 2 {
        fmt.Printf("名: %s, 姓: %s\n", parts[0], parts[1]) // 名: john, 姓: doe
    }
}

mermaid:

输入: "  hello   world  go   "
           ↓
       Fields()
           ↓
输出: ["hello", "world", "go"]

一句话总结: Fields 就是文字版的"切黄瓜"——不管刀工多乱,只留整齐的片(单词)。


11.10 strings.FieldsFunc:按自定义函数分割

专业解释: FieldsFunc 将字符串 s 按满足函数 f(c) 的字符分割,返回字符串切片。它是 Fields 的高级版本,允许你自定义分割规则。

通俗理解: Fields 是用"空白"这把刀切,FieldsFunc 则允许你自己磨刀——想按什么分就按什么分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    // 按逗号分割
    s1 := "apple,banana,cherry"
    parts1 := strings.FieldsFunc(s1, func(c rune) bool {
        return c == ','
    })
    fmt.Printf("按逗号分割: %q\n", parts1) // 按逗号分割: ["apple" "banana" "cherry"]

    // 按数字分割
    s2 := "abc123def456ghi"
    parts2 := strings.FieldsFunc(s2, unicode.IsDigit)
    fmt.Printf("按数字分割: %q\n", parts2) // 按数字分割: ["abc" "def" "ghi"]

    // 按字母分割
    s3 := "123abc456def"
    parts3 := strings.FieldsFunc(s3, unicode.IsLetter)
    fmt.Printf("按字母分割: %q\n", parts3) // 按字母分割: ["123" "456"]

    // 按空白字符分割(等效于 Fields)
    s4 := "hello\n\tworld"
    parts4 := strings.FieldsFunc(s4, unicode.IsSpace)
    fmt.Printf("按空白分割: %q\n", parts4) // 按空白分割: ["hello" "world"]

    // 实战:解析 CSV 简化版
    csvLine := "张三,25岁,工程师,北京"
    fields := strings.FieldsFunc(csvLine, func(c rune) bool {
        return c == ',' || c == ',' // 支持中英文逗号
    })
    fmt.Printf("CSV 字段: %q\n", fields) // CSV 字段: ["张三" "25岁" "工程师" "北京"]

    // 实战:按多个字符分割
    s5 := "apple;banana|cherry;grape"
    parts5 := strings.FieldsFunc(s5, func(c rune) bool {
        return c == ';' || c == '|'
    })
    fmt.Printf("按分号或竖线分割: %q\n", parts5) // 按分号或竖线分割: ["apple" "banana" "cherry" "grape"]
}

Fields vs FieldsFunc 对比:

函数分割依据灵活性
Fields空白字符(自动判断)固定
FieldsFunc自定义函数随心所欲

一句话总结: FieldsFunc 就是自定义刀具——你想怎么切,就怎么切。


11.11 strings.Split:按分隔符分割

专业解释: Split 将字符串 s 按分隔符 sep 分割,返回所有子串组成的切片。如果 sep 为空,Split 会将每个字符单独分割成一项。

通俗理解: 就像用刀把竹子切成段——Split 就是那个精准的伐木工,每到节点就切一刀。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    s := "apple|banana|cherry"
    parts := strings.Split(s, "|")
    fmt.Printf("Split 结果: %q\n", parts) // Split 结果: ["apple" "banana" "cherry"]

    // 分隔符不存在
    s2 := "apple banana cherry"
    parts2 := strings.Split(s2, "|")
    fmt.Printf("无分隔符: %q\n", parts2) // 无分隔符: ["apple banana cherry"]

    // 空字符串作为分隔符
    s3 := "hello"
    parts3 := strings.Split(s3, "")
    fmt.Printf("空分隔符分割: %q\n", parts3) // 空分隔符分割: ["h" "e" "l" "l" "o"]

    // 对比 SplitN(后面会详细讲)
    s4 := "a:b:c"
    parts4 := strings.Split(s4, ":")
    parts4n2 := strings.SplitN(s4, ":", 2)
    fmt.Printf("Split: %q\n", parts4)     // Split: ["a" "b" "c"]
    fmt.Printf("SplitN(2): %q\n", parts4n2) // SplitN(2): ["a" "b:c"]

    // 实战:解析路径
    path := "/usr/local/bin/go"
    dirs := strings.Split(path, "/")
    fmt.Printf("路径目录: %q\n", dirs) // 路径目录: ["" "usr" "local" "bin" "go"]

    // 实战:拆分 IP 地址
    ip := "192.168.1.100"
    segments := strings.Split(ip, ".")
    fmt.Printf("IP 分段: %q\n", segments) // IP 分段: ["192" "168" "1" "100"]
}

mermaid:

输入: "apple|banana|cherry"
分隔符: "|"

结果:
[0] "apple"
[1] "banana"
[2] "cherry"

一句话总结: Split 是"精准切割工"——遇到分隔符就切,不管前面切了几刀。


11.12 strings.SplitAfter:按分隔符分割,保留分隔符

专业解释: SplitAfter 按分隔符 sep 分割字符串,但保留分隔符在每个子串的末尾。这是 Split 和 SplitN 的"分家"兄弟。

通俗理解: Split 是"切完拿走刀",SplitAfter 是"切完刀还留在切面上"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
    "fmt"
    "strings"
)

func main() {
    // Split vs SplitAfter 对比
    s := "apple|banana|cherry"

    split := strings.Split(s, "|")
    splitAfter := strings.SplitAfter(s, "|")

    fmt.Printf("Split:      %q\n", split)      // Split:      ["apple" "banana" "cherry"]
    fmt.Printf("SplitAfter: %q\n", splitAfter) // SplitAfter: ["apple|" "banana|" "cherry"]

    // 空分隔符情况
    s2 := "hello"
    split2 := strings.Split(s2, "")
    splitAfter2 := strings.SplitAfter(s2, "")

    fmt.Printf("Split:      %q\n", split2)      // Split:      ["h" "e" "l" "l" "o"]
    fmt.Printf("SplitAfter: %q\n", splitAfter2) // SplitAfter: ["h" "e" "l" "l" "o"]

    // 实战:保留 URL 分隔符
    url := "https://example.com/path/to/page"
    parts := strings.SplitAfter(url, "/")
    fmt.Printf("URL 分段: %q\n", parts)
    // URL 分段: ["https:/" "//example.com" "/path" "/to" "/page"]

    // 实战:代码缩进分析
    codeLine := "    fmt.Println"
    parts2 := strings.SplitAfter(codeLine, "    ")
    fmt.Printf("缩进分析: %d 层\n", len(parts2)-1) // 缩进分析: 1 层
}

Split vs SplitAfter 对比:

原始字符串: "a|b|c"

Split:      ["a", "b", "c"]        ← 分隔符丢弃
SplitAfter: ["a|", "b|", "c"]     ← 分隔符保留

一句话总结: SplitAfter 就是"切完还留刀"版本,适合需要重建原始字符串的场景。


11.13 strings.SplitN:按分隔符分割,指定段数

专业解释: SplitN 将字符串 s 按分隔符 sep 分割,最多分割成 n 个子串。最后一个子串会包含剩余所有内容。如果 n <= 0,则返回空切片。

通俗理解: Split 是"能切多少切多少",SplitN 是"给我切 N-1 刀,剩下的打包"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    s := "a:b:c:d"

    fmt.Printf("SplitN(s, \":\", -1): %q\n", strings.SplitN(s, ":", -1))  // 全部分割
    fmt.Printf("SplitN(s, \":\", 0): %q\n", strings.SplitN(s, ":", 0))  // 不分割
    fmt.Printf("SplitN(s, \":\", 1): %q\n", strings.SplitN(s, ":", 1))  // 不分割,保留原串
    fmt.Printf("SplitN(s, \":\", 2): %q\n", strings.SplitN(s, ":", 2))  // 切 1 刀,分 2 段
    fmt.Printf("SplitN(s, \":\", 3): %q\n", strings.SplitN(s, ":", 3))  // 切 2 刀,分 3 段
    fmt.Printf("SplitN(s, \":\", 4): %q\n", strings.SplitN(s, ":", 4))  // 切 3 刀,分 4 段

    // 实战:取路径和文件名
    fullPath := "/usr/local/bin/program"
    parts := strings.SplitN(fullPath, "/", 3)
    // parts[0] = ""(/ 后第一个空)
    // parts[1] = "usr"
    // parts[2] = "local/bin/program"(剩余部分)
    fmt.Printf("路径前两段: %q\n", parts[:2]) // 路径前两段: ["" "usr"]

    // 实战:解析键值对
    kv := "name=john;age=25;city=beijing"
    parts2 := strings.SplitN(kv, ";", 2)
    fmt.Printf("键值对第一部分: %q\n", parts2[0]) // 键值对第一部分: "name=john"
    fmt.Printf("剩余部分: %q\n", parts2[1])       // 剩余部分: "age=25;city=beijing"

    // 实战:只取第一个字段
    s3 := "first,second,third,fourth"
    first := strings.SplitN(s3, ",", 2)[0]
    fmt.Printf("第一个字段: %s\n", first) // 第一个字段: first
}

SplitN 的 n 值效果:

n 值行为示例(“a:b:c:d”)
n < 0全部分割[“a”,“b”,“c”,“d”]
n == 0返回空切片[]
n == 1不分割,保留原串[“a:b:c:d”]
n == 2切 1 刀[“a”,“b:c:d”]
n == 3切 2 刀[“a”,“b”,“c:d”]

一句话总结: SplitN 就是"限量版分割"——告诉你要几段就给几段,最后一段打包剩余所有。


11.14 strings.HasPrefix、strings.HasSuffix:前后缀判断

专业解释: HasPrefix 检查字符串 s 是否以 prefix 开头,HasSuffix 检查是否以 suffix 结尾。两个函数都返回 bool,是日常判断路径、扩展名、协议等的常用工具。

通俗理解: HasPrefix 就是检查"你是不是XX开头的",HasSuffix 就是检查"你是不是XX结尾的"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    fmt.Println("HasPrefix(\"Hello\", \"He\"):", strings.HasPrefix("Hello", "He"))    // true
    fmt.Println("HasPrefix(\"Hello\", \"Go\"):", strings.HasPrefix("Hello", "Go"))   // false
    fmt.Println("HasSuffix(\"Hello\", \"lo\"):", strings.HasSuffix("Hello", "lo"))    // true
    fmt.Println("HasSuffix(\"Hello\", \"He\"):", strings.HasSuffix("Hello", "He"))   // false

    // 空字符串情况
    fmt.Println("HasPrefix(\"Hello\", \"\"):", strings.HasPrefix("Hello", ""))       // true
    fmt.Println("HasSuffix(\"Hello\", \"\"):", strings.HasSuffix("Hello", ""))        // true

    // 实战:文件类型判断
    func getFileType(filename string) string {
        if strings.HasSuffix(filename, ".pdf") {
            return "PDF 文档"
        } else if strings.HasSuffix(filename, ".doc") || strings.HasSuffix(filename, ".docx") {
            return "Word 文档"
        } else if strings.HasSuffix(filename, ".txt") {
            return "文本文件"
        } else if strings.HasSuffix(filename, ".jpg") || strings.HasSuffix(filename, ".png") {
            return "图片文件"
        }
        return "未知类型"
    }

    files := []string{"report.pdf", "notes.docx", "data.txt", "photo.png", "archive.zip"}
    for _, f := range files {
        fmt.Printf("%s: %s\n", f, getFileType(f))
    }

    // 实战:URL 类型判断
    func checkURL(url string) {
        if strings.HasPrefix(url, "https://") {
            fmt.Printf("%s: 安全连接 ✓\n", url)
        } else if strings.HasPrefix(url, "http://") {
            fmt.Printf("%s: 非安全连接 ⚠\n", url)
        } else if strings.HasPrefix(url, "ftp://") {
            fmt.Printf("%s: FTP 协议\n", url)
        }
    }
    checkURL("https://secure.com")   // https://secure.com: 安全连接 ✓
    checkURL("http://insecure.com")  // http://insecure.com: 非安全连接 ⚠

    // 实战:统一资源命名
    names := []string{"user:123", "user:456", "admin:789"}
    for _, name := range names {
        if strings.HasPrefix(name, "admin:") {
            fmt.Printf("%s 是管理员\n", name)
        }
    }
    // admin:789 是管理员
}

mermaid:

字符串: "Hello"
  HasPrefix?          HasSuffix?
       ↓                    ↓
    "He"?              "lo"?
       ↓                    ↓
    true               true

一句话总结: HasPrefix 和 HasSuffix 就是字符串版的"你是不是姓X"和"你是不是叫X"。


11.15 strings.Index 系列:Index、LastIndex、IndexByte、IndexRune、IndexFunc

专业解释: Index 系列函数用于查找子串或字符在字符串中的位置,返回 int 表示下标(从 0 开始),找不到返回 -1。包含 Index(子串)、LastIndex(最后出现位置)、IndexByte(单字节)、IndexRune(Unicode 码点)、IndexFunc(自定义函数)五种。

通俗理解: Index 就是"点名"——告诉你第几个位置找到了你要的东西。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    s := "Hello, World! Hello, Go!"

    // Index:子串第一次出现的位置
    fmt.Println("Index(\", \"):", strings.Index(s, ", "))           // 5
    fmt.Println("Index(\"World\"):", strings.Index(s, "World"))     // 7
    fmt.Println("Index(\"Python\"):", strings.Index(s, "Python"))   // -1

    // LastIndex:子串最后一次出现的位置
    fmt.Println("LastIndex(\"Hello\"):", strings.LastIndex(s, "Hello")) // 14(第二个 Hello 的位置)
    fmt.Println("LastIndex(\", \"):", strings.LastIndex(s, ", "))      // 13

    // IndexByte:查找单个字节
    fmt.Println("IndexByte('o'):", strings.IndexByte(s, 'o'))      // 4(第一个 'o')
    fmt.Println("LastIndexByte('o'):", strings.LastIndexByte(s, 'o')) // 21(最后一个 'o')

    // IndexRune:查找 Unicode 码点(中文)
    s2 := "你好世界"
    fmt.Println("IndexRune('好'):", strings.IndexRune(s2, '好'))   // 3(UTF-8 编码中,'好' 从字节索引 3 开始)
    fmt.Println("IndexRune('中'):", strings.IndexRune(s2, '中'))   // -1

    // IndexFunc:自定义查找函数
    s3 := "Hello123World456"
    idx := strings.IndexFunc(s3, unicode.IsDigit)
    fmt.Printf("第一个数字位置: %d(字符: %c)\n", idx, rune(s3[idx])) // 第一个数字位置: 5(字符: 1)

    idx2 := strings.LastIndexFunc(s3, unicode.IsDigit)
    fmt.Printf("最后一个数字位置: %d(字符: %c)\n", idx2, rune(s3[idx2])) // 最后一个数字位置: 15(字符: 6)

    // 实战:查找文件扩展名
    filename := "document.tar.gz"
    lastDot := strings.LastIndex(filename, ".")
    if lastDot > 0 {
        ext := filename[lastDot:]
        fmt.Printf("扩展名: %s\n", ext) // 扩展名: .gz
    }

    // 实战:查找路径分隔符
    path := "/home/user/documents/file.txt"
    lastSlash := strings.LastIndex(path, "/")
    dir := path[:lastSlash]
    fmt.Printf("目录: %s\n", dir) // 目录: /home/user/documents
}

Index 系列函数一览:

函数查找内容示例
Index(s, substr)子串Index(“Hello”, “ell”) → 1
LastIndex(s, substr)子串(最后一次)LastIndex(“Hello”, “l”) → 3
IndexByte(s, b)单字节IndexByte(“Hello”, ‘o’) → 4
IndexRune(s, r)Unicode 码点IndexRune(“你好”, ‘好’) → 3
IndexFunc(s, f)自定义函数IndexFunc(“abc”, unicode.IsLower) → 0

一句话总结: Index 系列就是字符串界的"定位雷达"——告诉你目标在哪里,找不到就报 -1。


11.16 strings.LastIndexAny:最后出现任意字符的位置

专业解释: LastIndexAny 从字符串 s 的右侧开始查找,返回 chars 中任意字符(每个字符单独匹配)最后出现的位置。如果找不到任何字符,返回 -1。

通俗理解: 就像从字符串的末尾往前扫描,找到 chars 里任何一个字符就停下——LastIndex 的"任意字符"版本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    s := "Hello, 世界!"

    fmt.Println("LastIndexAny(\"Hello\", \"el\"):", strings.LastIndexAny(s, "el"))     // 1('e' 位置)
    fmt.Println("LastIndexAny(\"Hello\", \"ol\"):", strings.LastIndexAny(s, "ol"))     // 4('o' 位置)
    fmt.Println("LastIndexAny(\"Hello\", \"xyz\"):", strings.LastIndexAny(s, "xyz"))   // -1

    // 从右向左找
    s2 := "abcd-efgh-ijkl"
    fmt.Println("LastIndexAny(\"abcd-efgh-ijkl\", \"-e\"):", strings.LastIndexAny(s2, "-e")) // 9('-' 在 efgh-ijkl 中的位置)

    // 中文场景
    s3 := "你好世界,你好中国"
    fmt.Println("LastIndexAny(\"你好世界,你好中国\", \"好\"):", strings.LastIndexAny(s3, "好")) // 10(最后一个'好'的位置)

    // 对比 LastIndex
    s4 := "ababa"
    fmt.Println("LastIndex(\"ababa\", \"aba\"):", strings.LastIndex(s4, "aba"))     // 2(子串 "aba" 最后出现位置)
    fmt.Println("LastIndexAny(\"ababa\", \"ab\"):", strings.LastIndexAny(s4, "ab")) // 4('b' 最后位置)

    // 实战:查找文件路径中的盘符或根目录
    filePath := "C:\\Users\\Documents\\file.txt"
    idx := strings.LastIndexAny(filePath, "\\/")
    if idx >= 0 {
        fmt.Printf("最后一个路径分隔符位置: %d\n", idx) // 最后一个路径分隔符位置: 16
    }

    // 实战:找最后一个数字位置
    s5 := "order123 items456 total789"
    idx2 := strings.LastIndexAny(s5, "0123456789")
    if idx2 >= 0 {
        fmt.Printf("最后一个数字 '%c' 在位置: %d\n", rune(s5[idx2]), idx2)
    }
}

LastIndex vs LastIndexAny:

字符串: "Hello, 世界!"
Chars:  "el"

LastIndex("Hello, 世界!", "el")
  → 1(子串 "el" 最后出现位置)

LastIndexAny("Hello, 世界!", "el")
  → 4('l' 最后出现位置,比 'e' 更靠右)

一句话总结: LastIndexAny 就是"从右往左扫,逮到哪个算哪个"。


11.17 strings.Join:拼接字符串数组

专业解释: Join 将字符串切片 elems 用分隔符 sep 连接成一个字符串。是 Split 的逆操作,是构建 CSV、路径拼接等场景的常用工具。

通俗理解: Join 就是"穿糖葫芦"——把字符串数组用一根线(分隔符)串起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    fruits := []string{"apple", "banana", "cherry"}
    result := strings.Join(fruits, ", ")
    fmt.Println("Join 结果:", result) // Join 结果: apple, banana, cherry

    // 空切片和单个元素
    empty := []string{}
    fmt.Println("空切片 Join:", strings.Join(empty, ",")) // 空切片 Join:

    single := []string{"only"}
    fmt.Println("单元素 Join:", strings.Join(single, "-")) // 单元素 Join: only

    // 无分隔符
    words := []string{"Go", "is", "awesome"}
    fmt.Println("无分隔符:", strings.Join(words, "")) // 无分隔符: Goisawesome

    // 实战:构建路径
    parts := []string{"usr", "local", "bin", "go"}
    path := strings.Join(parts, "/")
    fmt.Println("构建路径:", path) // 构建路径: usr/local/bin/go

    // 实战:生成 CSV
    headers := []string{"Name", "Age", "City"}
    csvHeader := strings.Join(headers, ",")
    fmt.Println("CSV 表头:", csvHeader) // CSV 表头: Name,Age,City

    // 实战:格式化输出
    tags := []string{"golang", "backend", "performance"}
    tagStr := "[" + strings.Join(tags, " | ") + "]"
    fmt.Println("标签:", tagStr) // 标签: [golang | backend | performance]

    // Join 和 Split 互为逆操作
    original := "a,b,c,d"
    split := strings.Split(original, ",")       // [a b c d]
    joined := strings.Join(split, ",")         // a,b,c
    fmt.Printf("Split+Join 可逆: %v → %v\n", split, joined == original) // true
}

mermaid:

输入: ["apple", "banana", "cherry"]
分隔符: ", "

Join 操作:
"apple" + ", " + "banana" + ", " + "cherry"

输出: "apple, banana, cherry"

一句话总结: Join 就是"串糖葫芦"——把零散的山楂(字符串)串成一串完整的糖葫芦。


11.18 strings.Map:字符映射转换

专业解释: Map 将字符串 s 中的每个字符通过映射函数 r 逐个转换,返回转换后的新字符串。如果映射函数返回负值,该字符会被删除(不保留)。

通俗理解: Map 就是一个字符流水线——每个字符进去,经过加工,变成新的字符出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    // 基本用法:替换字符
    s := "hello world"
    mapped := strings.Map(func(r rune) rune {
        return r + 1 // 每个字符 ASCII 码 +1:'a'→'b', 'b'→'c'...
    }, s)
    fmt.Println("字符+1:", mapped) // 字符+1: ifmmp!xpsme

    // 大小写反转
    s2 := "Hello World"
    swapped := strings.Map(func(r rune) rune {
        if unicode.IsLower(r) {
            return unicode.ToUpper(r)
        }
        if unicode.IsUpper(r) {
            return unicode.ToLower(r)
        }
        return r
    }, s2)
    fmt.Println("大小写反转:", swapped) // 大小写反转: hELLO wORLD

    // 删除特定字符
    s3 := "hello123world456"
    digitsRemoved := strings.Map(func(r rune) rune {
        if unicode.IsDigit(r) {
            return -1 // 返回 -1 表示删除
        }
        return r
    }, s3)
    fmt.Println("删除数字:", digitsRemoved) // 删除数字: helloworld

    // 只保留字母
    s4 := "Go1.18版本!"
    lettersOnly := strings.Map(func(r rune) rune {
        if unicode.IsLetter(r) || unicode.IsDigit(r) {
            return r
        }
        return -1
    }, s4)
    fmt.Println("只保留字母数字:", lettersOnly) // 只保留字母数字: Go118版本

    // 实战:ROT13 加密(凯撒密码)
    rot13 := func(r rune) rune {
        switch {
        case r >= 'a' && r <= 'z':
            return 'a' + (r-'a'+13)%26
        case r >= 'A' && r <= 'Z':
            return 'A' + (r-'A'+13)%26
        default:
            return r
        }
    }
    original := "Hello, World!"
    encoded := strings.Map(rot13, original)
    decoded := strings.Map(rot13, encoded) // ROT13 两次等于原文
    fmt.Printf("ROT13: %s → %s → %s\n", original, encoded, decoded)
    // ROT13: Hello, World! → Uryyb, Jbeyq! → Hello, World!

    // 实战:敏感词替换
    s5 := "这家餐厅的服务太差了!"
    sensitive := map[rune]rune{
        '差': '*',
        '烂': '#',
        '垃圾': '@', // Map 按字符处理,这个实际上只替换单个字符
    }
    replaced := strings.Map(func(r rune) rune {
        if repl, ok := sensitive[r]; ok {
            return repl
        }
        return r
    }, s5)
    fmt.Println("敏感词替换:", replaced) // 敏感词替换: 这家餐厅的服务太*了!
}

Map 执行流程:

输入: "abc"
  ↓
逐字符应用映射函数:
  'a' → f('a')
  'b' → f('b')
  'c' → f('c')
  ↓
组合成新字符串
  ↓
输出: "xyz"

一句话总结: Map 就是字符版的"流水线加工"——进去是生料,出来是成品。


11.19 strings.Replace、strings.ReplaceAll:替换

专业解释: Replace 将字符串 s 中的 old 子串替换为 new 子串,n 指定替换次数(n < 0 表示全部替换)。ReplaceAll 是 Replace 的便捷版本,相当于 n = -1。

通俗理解: Replace 就是"文字替换员"——告诉你替换几个,全部还是只换前几个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "foo-bar-foo-bar-foo"

    // Replace 替换指定次数
    fmt.Println("Replace(s, \"foo\", \"baz\", 1):", strings.Replace(s, "foo", "baz", 1))
    // foo-bar-foo-bar-foo → baz-bar-foo-bar-foo(只替换第一个)

    fmt.Println("Replace(s, \"foo\", \"baz\", 2):", strings.Replace(s, "foo", "baz", 2))
    // foo-bar-foo-bar-foo → baz-bar-baz-bar-foo(替换前两个)

    fmt.Println("Replace(s, \"foo\", \"baz\", -1):", strings.Replace(s, "foo", "baz", -1))
    // foo-bar-foo-bar-foo → baz-bar-baz-bar-baz(全部替换)

    // ReplaceAll
    fmt.Println("ReplaceAll:", strings.ReplaceAll(s, "foo", "baz"))
    // foo-bar-foo-bar-foo → baz-bar-baz-bar-baz(等效于 n=-1)

    // n=0 不替换
    fmt.Println("Replace(s, \"foo\", \"baz\", 0):", strings.Replace(s, "foo", "baz", 0))
    // foo-bar-foo-bar-foo → foo-bar-foo-bar-foo(不变)

    // 不存在的子串
    fmt.Println("Replace 不存在:", strings.Replace(s, "xyz", "123", -1))
    // foo-bar-foo-bar-foo → foo-bar-foo-bar-foo(不变)

    // 实战:模板替换
    template := "Hello, {name}! Welcome to {place}."
    replacements := []string{"{name}", "Alice", "{place}", "Wonderland"}
    result := template
    for i := 0; i < len(replacements); i += 2 {
        result = strings.Replace(result, replacements[i], replacements[i+1], 1)
    }
    fmt.Println("模板替换:", result) // Hello, Alice! Welcome to Wonderland.

    // 实战:URL 参数编码
    url := "name=张三&age=25"
    url = strings.ReplaceAll(url, "张三", "urlencode")
    fmt.Println("URL编码后:", url) // name=urlencode&age=25

    // 实战:格式化输出
    log := "ERROR: error occurred at line 100"
    info := strings.ReplaceAll(log, "ERROR", "INFO")
    warn := strings.ReplaceAll(log, "ERROR", "WARN")
    fmt.Println("ERROR→INFO:", info) // INFO: error occurred at line 100
    fmt.Println("ERROR→WARN:", warn) // WARN: error occurred at line 100
}

Replace n 值效果:

原始: "foo-bar-foo-bar-foo"
目标: "foo" → "baz"

n=0:  "foo-bar-foo-bar-foo" (不变)
n=1:  "baz-bar-foo-bar-foo" (只换第一个)
n=2:  "baz-bar-baz-bar-foo" (换前两个)
n=3:  "baz-bar-baz-bar-baz" (三个都换)
n=-1: "baz-bar-baz-bar-baz" (全部替换)

一句话总结: Replace 就是"选择性替换工"——说换几个就换几个,ReplaceAll 是"全换党"。


11.20 strings.Repeat:重复字符串

专业解释: Repeat 将字符串 s 重复 count 次,返回一个新字符串。如果 count 为负或内存分配过大,会 panic。

通俗理解: Repeat 就是"复制粘贴"——一次复制 N 份你想要的内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    fmt.Println("Repeat(\"ha\", 3):", strings.Repeat("ha", 3))      // hahaha
    fmt.Println("Repeat(\"*\", 5):", strings.Repeat("*", 5))       // *****
    fmt.Println("Repeat(\"Go!\", 2):", strings.Repeat("Go!", 2))   // Go!Go!

    // count=0 返回空字符串
    fmt.Println("Repeat(\"hi\", 0):", strings.Repeat("hi", 0))     // (空)

    // 实战:生成分隔线
    width := 50
    line := strings.Repeat("-", width)
    fmt.Println(line) // --------------------------------------------------

    // 实战:缩进
    indent := strings.Repeat("    ", 3) // 3 个 Tab
    code := indent + "fmt.Println(\"Hello\")"
    fmt.Println(code)
    //             fmt.Println("Hello")

    // 实战:构建表格边框
    colWidth := 10
    cell := strings.Repeat("-", colWidth)
    border := "+" + cell + "+" + cell + "+"
    fmt.Println(border) // +----------+----------+

    // 实战:调试可视化
    progress := 0.75
    barWidth := 20
    filled := int(float64(barWidth) * progress)
    bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
    fmt.Printf("进度: [%s] %.0f%%\n", bar, progress*100)
    // 进度: [████████████████░░░░] 75%

    // 实战:生成 UUID(简化版)
    part := "abcd"
    uuid := strings.Repeat(part, 4)
    fmt.Printf("简化 UUID: %s\n", uuid) // abcdabcdabcdabcd
}

mermaid:

Repeat("ab", 3)

"ab" + "ab" + "ab"

→ "ababab"

一句话总结: Repeat 就是"Ctrl+C 无限按"——想要几个复制几个。


11.21 strings.ToLower、strings.ToUpper:大小写转换

专业解释: ToLower 将字符串中所有 ASCII 字符转为小写(非 ASCII 的 Unicode 字符保持不变);ToUpper 将所有 ASCII 字符转为大写。它们都返回转换后的新字符串。

通俗理解: ToLower 就是"降魔大法"——所有大写字母都乖乖变成小写;ToUpper 就是"升职大法"——小写字母集体升职变成大写。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    fmt.Println("ToLower(\"HELLO\"):", strings.ToLower("HELLO"))    // hello
    fmt.Println("ToUpper(\"hello\"):", strings.ToUpper("hello"))    // HELLO
    fmt.Println("ToLower(\"Hello\"):", strings.ToLower("Hello"))  // hello
    fmt.Println("ToUpper(\"HeLLo\"):", strings.ToUpper("HeLLo"))  // HELLO

    // Unicode 处理(中文不变)
    fmt.Println("ToLower(\"你好\"):", strings.ToLower("你好"))      // 你好(不变)
    fmt.Println("ToUpper(\"你好\"):", strings.ToUpper("你好"))      // 你好(不变)

    // 已经是小写/大写的不变
    fmt.Println("ToLower(\"abc\"):", strings.ToLower("abc"))      // abc
    fmt.Println("ToUpper(\"ABC\"):", strings.ToUpper("ABC"))      // ABC

    // 数字和符号不变
    fmt.Println("ToLower(\"GO123!\"):", strings.ToLower("GO123!")) // go123!
    fmt.Println("ToUpper(\"go123!\"):", strings.ToUpper("go123!")) // GO123!

    // 实战:大小写不敏感比较(已讲过的 EqualFold)
    s1, s2 := "Hello", "HELLO"
    if strings.ToLower(s1) == strings.ToLower(s2) {
        fmt.Println("两者相等(通过 ToLower 比较)")
    }

    // 实战:规范化用户输入
    userInput := "ADMIN"
    normalized := strings.ToLower(userInput)
    fmt.Printf("规范化后的用户名: %s\n", normalized) // admin

    // 实战:首字母大写(结合操作)
    word := "golang"
    if len(word) > 0 {
        title := strings.ToUpper(string(word[0])) + word[1:]
        fmt.Println("首字母大写:", title) // Golang
    }

    // 实战:构造常量字符串
    const key = "API_KEY"
    fmt.Println("常量键:", strings.ToLower(key)) // api_key
}

ToLower 和 ToUpper 作用域:

ASCII 字母: a-z, A-Z → 转换
其他字符: 数字、符号、中文等 → 保持不变

一句话总结: ToLower/ToUpper 就是字符串的"升降机"——载着字母上下移动。


11.22 strings.ToLowerSpecial、strings.ToUpperSpecial:指定 Unicode 分类的大小写转换

专业解释: ToLowerSpecial 和 ToUpperSpecial 允许你指定一个 unicode.SpecialCase 映射表,用于特定语言或 Unicode 分类的大小写转换。例如土耳其语的 I/i 问题(点是 I,无点是 ı)。

通俗理解: 普通的大小写转换是"普通话",Special 版本就是"方言"——针对特定语言定制转换规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    // 普通转换
    fmt.Println("ToLower(\"Istanbul\"):", strings.ToLower("Istanbul"))  // istanbul

    // 土耳其语特殊规则:
    // 拉丁字母 I (ASCII) → 土耳其语 ı (无点 i)
    // 拉丁字母 İ (ASCII) → 土耳其语 i (点 i 变成小写 i)
    // 使用 Turkish UpperCase 规则
    turkishUpper := strings.ToUpperSpecial(unicode.TurkishCase, "i")
    fmt.Println("土耳其语 ToUpper(\"i\"):", turkishUpper) // İ(有点的 I)

    turkishLower := strings.ToLowerSpecial(unicode.TurkishCase, "İ")
    fmt.Println("土耳其语 ToLower(\"İ\"):", turkishLower) // i(无点的 i)

    // 对比普通转换
    normalUpper := strings.ToUpper("i")
    fmt.Println("普通 ToUpper(\"i\"):", normalUpper) // I

    // Custom SpecialCase 示例
    custom := unicode.SpecialCase{
        unicode.CaseRange{
            Lo: 0x61, // 'a'
            Hi: 0x7A, // 'z'
            Delta: [unicode.MaxCase]int32{0, 0, 0}, // 全部变成 0(小写不变)
        },
    }

    // 实战:处理多语言文本
    texts := []string{
        "Istanbul",
        "İstanbul",   // 土耳其语写法
        "Αθήνα",      // 希腊语
        "Москва",     // 俄语
    }

    fmt.Println("\n=== 多语言文本转换 ===")
    for _, t := range texts {
        lower := strings.ToLower(t)
        upper := strings.ToUpper(t)
        fmt.Printf("原文: %-12s 小写: %-12s 大写: %-12s\n", t, lower, upper)
    }
}

土耳其语大小写特殊规则(重点):

拉丁字母 I → 土耳其语 ı (无点的小写 i)
拉丁字母 İ → 土耳其语 i (带点的大写 I)

这对 "i" 和 "I" 互换了!
普通: "I" → "i"
土耳其: "I" → "ı"

一句话总结: Special 版本是"方言版"大小写——当你的程序需要处理土耳其语等特殊语言时,这两兄弟就派上用场了。


11.23 strings.Title、strings.ToTitle:首字母大写

专业解释: Title 将每个单词的首字母大写(更准确地说,是将每个 Unicode 字母的 title case 置位);ToTitle 将所有字母转为 title case(即每个字符都变成其 title case)。

通俗理解: Title 就是"标题化"——每个单词开头都大写,看起来像文章的标题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    fmt.Println("Title(\"hello world\"):", strings.Title("hello world"))    // Hello World
    fmt.Println("ToTitle(\"hello world\"):", strings.ToTitle("hello world")) // HELLO WORLD

    // 对比
    s := "the quick brown fox"
    fmt.Println("Title:", strings.Title(s))    // The Quick Brown Fox
    fmt.Println("ToTitle:", strings.ToTitle(s)) // THE QUICK BROWN FOX

    // 数字和标点不变
    fmt.Println("Title(\"100 dollars\"):", strings.Title("100 dollars"))  // 100 Dollars

    // 连续大写不变
    fmt.Println("Title(\"API SPEC\"):", strings.Title("API SPEC"))          // API SPEC

    // 实战:格式化标题
    article := "the history of golang"
    title := strings.Title(article)
    fmt.Println("文章标题:", title) // The History Of Golang

    // 实战:数据库字段格式化
    column := "first_name"
    snakeToTitle := strings.Title(strings.Replace(column, "_", " ", -1))
    fmt.Println("字段名:", snakeToTitle) // First Name

    // 实战:用户输入格式化
    func formatName(input string) string {
        words := strings.Fields(input)
        for i, word := range words {
            if len(word) > 0 {
                words[i] = strings.ToUpper(word[:1]) + strings.ToLower(word[1:])
            }
        }
        return strings.Join(words, " ")
    }

    names := []string{"john doe", "JANE SMITH", "alice"}
    for _, n := range names {
        fmt.Printf("格式化: %s → %s\n", n, formatName(n))
    }
    // john doe → John Doe
    // JANE SMITH → Jane Smith
    // alice → Alice
}

Title vs ToTitle:

输入: "the quick brown fox"

Title:     "The Quick Brown Fox"     ← 每个单词首字母大写
ToTitle:   "THE QUICK BROWN FOX"     ← 所有字母都变大写(Title Case)
ToUpper:   "THE QUICK BROWN FOX"     ← 所有字母都变大写(Upper Case)

一句话总结: Title 就是"标题党"——每个单词都得有个大写脑袋。


11.24 strings.Trim 系列:Trim、TrimLeft、TrimRight、TrimSpace、TrimPrefix、TrimSuffix

专业解释: Trim 系列函数用于去除字符串两端的指定字符或空白。Trim 去除两端、TrimLeft 只去左端、TrimRight 只去右端、TrimSpace 去除所有空白、TrimPrefix 去除前缀、TrimSuffix 去除后缀。

通俗理解: 就像清理字符串的"边缘地带"——Trim 把两边都收拾干净,TrimLeft 只收拾左边,TrimRight 只收拾右边。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
    "fmt"
    "strings"
)

func main() {
    // Trim:去除两端指定字符
    fmt.Println("Trim(\"  hello  \"):", strings.Trim("  hello  ", " "))       // hello
    fmt.Println("Trim(\"##hello##\", \"#\"):", strings.Trim("##hello##", "#")) // hello
    fmt.Println("Trim(\"...hello...\", \"...\"):", strings.Trim("...hello...", ".")) // hello

    // TrimSpace:去除所有空白字符
    fmt.Println("TrimSpace(\"  \t\nhello\r\n  \"):", strings.TrimSpace("  \t\nhello\r\n  ")) // hello

    // TrimLeft / TrimRight
    fmt.Println("TrimLeft(\"  hello\", \" \"):", strings.TrimLeft("  hello", " "))   // hello
    fmt.Println("TrimRight(\"hello   \", \" \"):", strings.TrimRight("hello   ", " ")) // hello

    // TrimPrefix / TrimSuffix:去除前缀/后缀
    s := "Hello, World!"
    fmt.Println("TrimPrefix(\"Hello,\", \"Hello,\"):", strings.TrimPrefix(s, "Hello,")) // World!
    fmt.Println("TrimSuffix(\"!\", \"!\"):", strings.TrimSuffix(s, "!"))               // Hello, World

    // 空白字符类型
    fmt.Println("TrimSpace 含换行:", strings.TrimSpace("\n\t hello \t\n")) // hello

    // 实战:清理用户输入
    userInput := "   john   "
    name := strings.TrimSpace(userInput)
    fmt.Printf("清理后用户名: %s\n", name) // john

    // 实战:去除 URL 参数前缀
    param := "key=value"
    if strings.HasPrefix(param, "key=") {
        value := strings.TrimPrefix(param, "key=")
        fmt.Printf("参数值: %s\n", value) // value
    }

    // 实战:去除文件扩展名
    filename := "document.pdf"
    if strings.HasSuffix(filename, ".pdf") {
        nameWithoutExt := strings.TrimSuffix(filename, ".pdf")
        fmt.Printf("无扩展名: %s\n", nameWithoutExt) // document
    }

    // 实战:解析命令行参数
    arg := "--verbose"
    if strings.TrimPrefix(arg, "-") == "" || strings.TrimPrefix(arg, "--") == "" {
        fmt.Println("这是一个标志参数")
    } else {
        key := strings.TrimPrefix(arg, "--")
        fmt.Printf("命名参数: %s\n", key) // verbose
    }

    // 实战:去除引号
    quoted := "\"hello\""
    unquoted := strings.Trim(quoted, "\"")
    fmt.Printf("去引号: %s\n", unquoted) // hello
}

Trim 系列全家福:

原始: "  hello world  "

TrimLeft:  "hello world  "     ← 只去左边
TrimRight: "  hello world"     ← 只去右边
TrimSpace: "hello world"       ← 去掉两端空白
Trim:      "hello world"       ← 去掉两端指定字符(这里是空白)

一句话总结: Trim 系列就是"边境清理队"——决定清理哪一边的边境,由你说了算。


11.25 strings.TrimFunc、TrimLeftFunc、TrimRightFunc:按自定义函数修剪

专业解释: TrimFunc 系列接受一个函数参数,根据函数的返回值决定是否去除字符。它是 Trim 系列的自定义版本,TrimLeftFunc 去除左边满足条件的字符,TrimRightFunc 去除右边,TrimFunc 去除两边。

通俗理解: 普通 Trim 用固定规则去字符,TrimFunc 用自定义规则——你想怎么去就怎么去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    // TrimFunc:去除两端满足条件的字符
    s := "!!!hello!!!"
    trimmed := strings.TrimFunc(s, func(r rune) bool {
        return r == '!'
    })
    fmt.Println("TrimFunc:", trimmed) // hello

    // TrimLeftFunc:只去左边
    s2 := "123abc456"
    leftTrimmed := strings.TrimLeftFunc(s2, unicode.IsDigit)
    fmt.Println("TrimLeftFunc:", leftTrimmed) // abc456(去掉了左边的数字)

    // TrimRightFunc:只去右边
    rightTrimmed := strings.TrimRightFunc(s2, unicode.IsDigit)
    fmt.Println("TrimRightFunc:", rightTrimmed) // 123abc(去掉了右边的数字)

    // 自定义条件:去除非字母字符
    s3 := "...abc...xyz..."
    alphaOnly := strings.TrimFunc(s3, func(r rune) bool {
        return !unicode.IsLetter(r)
    })
    fmt.Println("去除非字母:", alphaOnly) // abc...xyz(只去两边)

    // 实战:去除首尾空白和标点
    dirty := "  !!hello world??  "
    cleaned := strings.TrimFunc(dirty, func(r rune) bool {
        return unicode.IsSpace(r) || r == '!' || r == '?'
    })
    fmt.Println("清理后:", cleaned) // hello world

    // 实战:提取字符串中的有效部分
    data := "xxx123456yyy"
    digits := strings.TrimFunc(data, func(r rune) bool {
        return !unicode.IsDigit(r)
    })
    fmt.Println("提取数字:", digits) // 123456

    // 实战:去除 HTML 标签属性
    tag := `<a href="test.html" title="链接">`
    content := strings.TrimFunc(tag, func(r rune) bool {
        return r == '<' || !unicode.IsLetter(r)
    })
    // 简化示例,实际用正则更靠谱
    _ = content
}

TrimFunc 系列执行流程:

输入: "!!!hello!!!"
函数: r == '!'

从左扫描: '!' → true → 去除
从左扫描: '!' → true → 去除
从左扫描: '!' → true → 去除
遇到 'h' → false → 停止
从右扫描: '!' → true → 去除
...
最终: "hello"

一句话总结: TrimFunc 就是"自定义剪刀"——你自己决定哪边该剪掉。


11.26 strings.Builder:高效拼接字符串

专业解释: Builder 是一个用于高效构建字符串的类型。它内部维护一个 []byte 缓冲区,通过 WriteString 等方法追加内容,比普通的 + 拼接更高效(避免频繁内存分配)。

通俗理解: 普通字符串拼接像"粘贴-复制-粘贴-复制",Builder 像"往盒子里扔东西"——更高效,不浪费。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 创建 Builder
    var builder strings.Builder

    // 使用 WriteString 追加内容
    builder.WriteString("Hello")
    builder.WriteString(", ")
    builder.WriteString("World!")

    // 获取构建好的字符串
    result := builder.String()
    fmt.Println("Builder 结果:", result) // Hello, World!

    // 查看已写入的字节数
    fmt.Println("已写入字节数:", builder.Len()) // 13

    // 实战:构建 HTML
    var html strings.Builder
    html.WriteString("<html>")
    html.WriteString("<head><title>Test</title></head>")
    html.WriteString("<body>")
    html.WriteString("<h1>Hello, World!</h1>")
    html.WriteString("</body>")
    html.WriteString("</html>")
    fmt.Println("\nHTML 输出:")
    fmt.Println(html.String())

    // 实战:构建 CSV 行
    func buildCSVRow(values ...string) string {
        var b strings.Builder
        for i, v := range values {
            if i > 0 {
                b.WriteString(",")
            }
            b.WriteString(v)
        }
        return b.String()
    }
    fmt.Println("CSV 行:", buildCSVRow("张三", "25", "北京"))

    // 实战:条件拼接
    parts := []string{"item1", "item2", "item3"}
    var list strings.Builder
    list.WriteString("[")
    for i, p := range parts {
        if i > 0 {
            list.WriteString(", ")
        }
        list.WriteString(p)
    }
    list.WriteString("]")
    fmt.Println("列表:", list.String()) // [item1, item2, item3]

    // 多次使用 String()
    builder2 := strings.Builder{}
    builder2.WriteString("Go ")
    builder2.WriteString("is ")
    builder2.WriteString("awesome!")
    s1 := builder2.String()
    s2 := builder2.String()
    fmt.Printf("s1 和 s2 相同? %v\n", s1 == s2) // true(内容相同)
    fmt.Printf("s1 地址: %p, s2 地址: %p\n", &s1, &s2) // 不同地址
}

Builder vs 字符串拼接:

普通拼接(效率低):
s := ""
for i := 0; i < 1000; i++ {
    s += "item" // 每次都分配新内存!
}

Builder 拼接(效率高):
var b strings.Builder
for i := 0; i < 1000; i++ {
    b.WriteString("item") // 增量追加,内存高效复用
}
result := b.String()

一句话总结: Builder 就是"字符串工厂"——批量生产字符串,省时省力。


11.27 strings.Builder.WriteString:写入字符串

专业解释: WriteString 将字符串 s 的内容追加到 Builder 内部缓冲区。它返回写入的字节数(通常等于 len(s))和可能的错误。

通俗理解: WriteString 就是往 Builder 这个"盒子"里扔东西的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
    "fmt"
    "strings"
)

func main() {
    var builder strings.Builder

    // 写入字符串
    n, err := builder.WriteString("Hello")
    fmt.Printf("写入 %d 字节: %v\n", n, err) // 写入 5 字节: <nil>

    n, err = builder.WriteString(", ")
    fmt.Printf("写入 %d 字节: %v\n", n, err) // 写入 2 字节: <nil>

    n, err = builder.WriteString("World!")
    fmt.Printf("写入 %d 字节: %v\n", n, err) // 写入 6 字节: <nil>

    fmt.Println("结果:", builder.String()) // Hello, World!
    fmt.Println("总长度:", builder.Len())  // 13

    // 写入空字符串
    n, err = builder.WriteString("")
    fmt.Printf("写入空字符串: %d 字节\n", n) // 写入空字符串: 0 字节

    // 实战:写入 rune
    builder2 := strings.Builder{}
    builder2.WriteString("Go ")
    // WriteRune 写入单个 rune
    r, size := builder2.WriteRune('👍')
    fmt.Printf("写入 rune: %c, 大小: %d 字节\n", r, size) // 写入 rune: 👍, 大小: 4 字节
    fmt.Println("Builder 内容:", builder2.String()) // Go 👍

    // 实战:写入字节切片(Write 的行为)
    builder3 := strings.Builder{}
    byteSlice := []byte("Binary data: ")
    n, err = builder3.Write(byteSlice)
    fmt.Printf("写入字节: %d 字节\n", n) // 写入字节: 13 字节
    builder3.Write([]byte{65, 66, 67}) // ABC
    fmt.Println("Builder:", builder3.String()) // Binary data: ABC

    // 实战:构建消息
    func buildMessage(parts ...string) string {
        var b strings.Builder
        for _, part := range parts {
            b.WriteString(part)
        }
        return b.String()
    }
    fmt.Println("消息:", buildMessage("[INFO] ", "服务启动", " - ", "端口: 8080"))
}

WriteString vs WriteByte vs WriteRune:

方法参数类型说明
WriteString(s string)string写入字符串
WriteByte(b byte)byte写入单个字节
WriteRune(r rune)rune写入单个 UTF-8 rune

一句话总结: WriteString 是 Builder 的"主要投料口"——字符串从这里进去,组合好的成品从 String() 出来。


11.28 strings.Builder.String:返回构建好的字符串

专业解释: String 返回 Builder 内部缓冲区中的所有内容作为 string。它返回一个字符串切片,不会复制底层数据(只是引用)。

通俗理解: String 就是"从盒子里取出成品"——把之前扔进去的所有东西拼成一个完整的字符串拿出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
    "fmt"
    "strings"
)

func main() {
    var builder strings.Builder

    // 追加内容
    builder.WriteString("Hello")
    builder.WriteString(", ")
    builder.WriteString("World!")

    // String() 返回构建好的字符串
    result := builder.String()
    fmt.Println("String():", result) // Hello, World!
    fmt.Println("Len():", builder.Len()) // 13

    // String() 可以多次调用
    s1 := builder.String()
    s2 := builder.String()
    fmt.Printf("s1 == s2: %v\n", s1 == s2) // true(内容相同)

    // String() 不消耗 Builder 内容,可以继续追加
    builder.WriteString(" Continue...")
    fmt.Println("继续追加后:", builder.String())
    // Hello, World! Continue...

    // 实战:最终输出前构建
    func generateReport(items []string) string {
        var b strings.Builder
        b.WriteString("=== 报告 ===\n")
        for i, item := range items {
            b.WriteString(fmt.Sprintf("%d. %s\n", i+1, item))
        }
        b.WriteString("=== 结束 ===")
        return b.String()
    }

    items := []string{"销售增长 20%", "用户突破 100 万", "系统稳定性 99.9%"}
    fmt.Println(generateReport(items))

    // 实战:JSON 拼接
    func buildJSON(key, value string) string {
        var b strings.Builder
        b.WriteString(`{"`)
        b.WriteString(key)
        b.WriteString(`":"`)
        b.WriteString(value)
        b.WriteString(`"}`)
        return b.String()
    }
    fmt.Println("JSON:", buildJSON("name", "张三")) // {"name":"张三"}
}

String() 注意事项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// String() 不会清空 Builder,可以多次调用
var b strings.Builder
b.WriteString("hello")
fmt.Println(b.String()) // hello
b.WriteString(" world")
fmt.Println(b.String()) // hello world

// 如果想清空,重新创建 Builder
b = strings.Builder{}
b.WriteString("new content")
fmt.Println(b.String()) // new content

一句话总结: String 就是"取货"操作——Builder 里攒的东西,一次性拿出来。


11.29 strings.Reader:把字符串变成 Reader

专业解释: Reader 是一个实现 io.Reader、io.ReaderAt、io.Seeker、io.WriterTo、io.ByteScanner 等接口的类型。它将字符串包装成可随机访问的读取器,支持从任意位置读取。

通俗理解: 普通的字符串只能"从左到右读一遍",Reader 把字符串变成"磁带"——可以快进、倒退、跳转到任意位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 创建 Reader
    r := strings.NewReader("Hello, World!")

    // 查看 Reader 信息
    fmt.Println("Size:", r.Size()) // 13
    fmt.Println("Len:", r.Len())   // 13(剩余可读字节)

    // 读取全部
    buf := make([]byte, r.Size())
    n, _ := r.Read(buf)
    fmt.Printf("读取 %d 字节: %q\n", n, string(buf)) // 读取 13 字节: "Hello, World!"
}

一句话总结: NewReader 就是把"静态字符串"变成"可操控的流"。


11.30 strings.Reader.Read:读取字节

专业解释: Read 将最多 len(p) 个字节读取到 p 中,返回读取的字节数和任何错误。如果到达文件末尾,返回 io.EOF。

通俗理解: Read 就是"从磁带里取一截内容出来"——取完后磁头就往后挪了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
    "fmt"
    "strings"
)

func main() {
    r := strings.NewReader("Hello, World!")

    // 读取前 5 个字节
    p := make([]byte, 5)
    n, err := r.Read(p)
    fmt.Printf("Read: n=%d, err=%v, data=%q\n", n, err, string(p))
    // Read: n=5, err=<nil>, data="Hello"

    // 继续读取接下来的 5 个字节
    n, err = r.Read(p)
    fmt.Printf("Read: n=%d, err=%v, data=%q\n", n, err, string(p))
    // Read: n=5, err=<nil>, data=", Wor"

    // 再次读取
    n, err = r.Read(p)
    fmt.Printf("Read: n=%d, err=%v, data=%q\n", n, err, string(p))
    // Read: n=3, err=<nil>, data="ld!"

    // 再读就 EOF 了
    n, err = r.Read(p)
    fmt.Printf("Read: n=%d, err=%v\n", n, err)
    // Read: n=0, err=EOF

    // 实战:按行读取(简单版)
    r2 := strings.NewReader("line1\nline2\nline3\n")
    lineBuf := make([]byte, 0, 20)
    for {
        b := make([]byte, 1)
        _, err := r2.Read(b)
        if err != nil {
            break
        }
        if b[0] == '\n' {
            fmt.Printf("行: %s\n", string(lineBuf))
            lineBuf = lineBuf[:0]
        } else {
            lineBuf = append(lineBuf, b[0])
        }
    }
    if len(lineBuf) > 0 {
        fmt.Printf("最后一行: %s\n", string(lineBuf))
    }
}

Read 执行流程:

Reader 内容: "Hello, World!"
               ↑
           磁头位置

Read(p, 5) → "Hello"
磁头移动到位置 5

Read(p, 5) → ", Wor"
磁头移动到位置 10

Read(p, 5) → "ld!"
磁头移动到位置 13

Read(p, 5) → io.EOF

一句话总结: Read 就是"按量取货"——每次取多少由你定,取完磁头就往前挪。


11.31 strings.Reader.Seek:跳转读写位置

专业解释: Seek 设置下一次 Read 或 Write 的偏移量。whence 可以是 io.SeekStart(相对于起始)、io.SeekCurrent(相对于当前位置)、io.SeekEnd(相对于末尾)。

通俗理解: Seek 就是"遥控器上的快进快退键"——想跳到哪里就跳到哪里。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
    "fmt"
    "io"
    "strings"
)

func main() {
    r := strings.NewReader("Hello, World!")

    // 从位置 0 开始读 5 字节
    p := make([]byte, 5)
    r.Read(p)
    fmt.Printf("读到: %q\n", string(p)) // "Hello"

    // Seek 到位置 7("World!" 的开始)
    offset, err := r.Seek(7, io.SeekStart)
    fmt.Printf("Seek 到起始位置 7, 实际偏移: %d, err: %v\n", offset, err)

    // 再读就是 "World"
    r.Read(p)
    fmt.Printf("读到: %q\n", string(p)) // "World"

    // Seek 到末尾
    r.Seek(0, io.SeekEnd)
    remaining := make([]byte, r.Len())
    r.Read(remaining)
    fmt.Printf("从末尾读: %q\n", string(remaining)) // "!"

    // Seek 相对当前位置
    r.Seek(0, io.SeekStart) // 重置到开头
    r.Seek(3, io.SeekCurrent) // 从位置 0 往后跳 3
    p2 := make([]byte, 3)
    r.Read(p2)
    fmt.Printf("从当前位置跳转后读: %q\n", string(p2)) // "lo,"

    // 实战:随机读取
    r2 := strings.NewReader("0123456789")
    for _, pos := range []int64{9, 3, 7, 1} {
        r2.Seek(pos, io.SeekStart)
        b := make([]byte, 1)
        r2.Read(b)
        fmt.Printf("位置 %d: %c\n", pos, b[0])
    }
    // 位置 9: 9
    // 位置 3: 3
    // 位置 7: 7
    // 位置 1: 1
}

Seek whence 参数:

whence名称说明
0SeekStart相对于起始位置
1SeekCurrent相对于当前位置
2SeekEnd相对于末尾位置

一句话总结: Seek 就是"时间旅行机"——想去哪个时间点就去哪个时间点。


11.32 strings.Reader.Size:原始字符串长度

专业解释: Size 返回原始字符串的字节数(UTF-8 编码后的字节长度),这是一个常量,不会因为读取而改变。

通俗理解: Size 就是告诉你"这个磁带总共有多长"——不管读到哪里,总长度不变。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
    "fmt"
    "strings"
)

func main() {
    // ASCII 字符串
    r1 := strings.NewReader("Hello")
    fmt.Println("ASCII Size:", r1.Size()) // 5

    // 中文字符串(UTF-8 编码)
    r2 := strings.NewReader("你好")
    fmt.Println("中文 Size:", r2.Size()) // 6(每个中文字符 3 字节)

    // Emoji 字符串
    r3 := strings.NewReader("👋")
    fmt.Println("Emoji Size:", r3.Size()) // 4(emoji 通常占 4 字节)

    // 混合字符串
    r4 := strings.NewReader("Go1.18新特性")
    fmt.Println("混合 Size:", r4.Size()) // 13

    // Size vs Len
    r5 := strings.NewReader("Hello, World!")
    fmt.Printf("Size: %d, Len: %d\n", r5.Size(), r5.Len()) // Size: 13, Len: 13

    r5.Read(make([]byte, 5)) // 读走 5 字节
    fmt.Printf("读取后 - Size: %d, Len: %d\n", r5.Size(), r5.Len())
    // 读取后 - Size: 13, Len: 8(Size 不变,Len 减少)

    // 实战:预分配缓冲区
    r6 := strings.NewReader("大量文本内容...")
    buf := make([]byte, r6.Size()) // 预分配正确大小的缓冲区
    n, _ := r6.Read(buf)
    fmt.Printf("读取了 %d 字节\n", n)
}

Size vs Len:

字符串: "Hello, World!"
Size: 13(原始总长度,不变)
Len: 13 → 8 → 0(剩余可读长度,随读取递减)

一句话总结: Size 就是"总里程表"——永远显示这卷磁带的总长度。


11.33 strings.Reader.WriteTo:写入目标 Writer

专业解释: WriteTo 将 Reader 中的所有剩余内容写入 io.Writer 接口。它实现了 io.WriterTo 接口,可以一次性把 Reader 内容写入目标。

通俗理解: WriteTo 就是"把磁带内容倒灌进音箱"——从当前位置到末尾,全部倒出去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 创建 Reader
    r := strings.NewReader("Hello, World!")

    // 读取部分内容
    p := make([]byte, 5)
    r.Read(p)
    fmt.Printf("已读: %q\n", string(p)) // "Hello"

    // WriteTo 将剩余内容写入 os.Stdout
    n, err := r.WriteTo(fmt.Stdout)
    fmt.Printf("\nWriteTo 写入 %d 字节\n", n) // , World!
    // 注意:上面 fmt.Stdout 输出在 n 之前

    // 实战:写入文件
    // 注意:实际使用 os.WriteFile 或 bufio.Writer 更方便
    // 这里演示 WriteTo 接口
    r2 := strings.NewReader("写入目标的内容")
    var output strings.Builder
    written, _ := r2.WriteTo(&output)
    fmt.Printf("WriteTo 写入 %d 字节到 Builder: %s\n", written, output.String())

    // 实战:多次 WriteTo
    r3 := strings.NewReader("可重复读取的内容")
    var dest1, dest2 strings.Builder

    r3.WriteTo(&dest1)
    fmt.Println("第一次:", dest1.String())

    // 注意:WriteTo 会消费 Reader,所以第二次是空的
    written2, _ := r3.WriteTo(&dest2)
    fmt.Printf("第二次(剩余): %d 字节\n", written2) // 0

    // 如果要重复使用,需要重置 Reader
    r3 = strings.NewReader("再次读取")
    r3.WriteTo(&dest2)
    fmt.Println("重置后第二次:", dest2.String())
}

WriteTo 执行流程:

Reader: "Hello, World!"
        ↑ 磁头位置(已读过 "Hello")

WriteTo(os.Stdout)
  → 从磁头位置开始
  → ", World!"
  → 全部写入目标

一句话总结: WriteTo 就是"批量倾倒"——把 Reader 里剩下的东西一次性倒给目标。


11.34 strings.Clone:复制字符串(Go 1.18+)

专业解释: Clone 返回字符串 s 的副本。如果 s 为空字符串或 nil,返回空字符串。这是 Go 1.18 引入的函数,用于安全地复制字符串(避免共享底层数组的风险)。

通俗理解: Clone 就是"克隆"——一模一样的复制品,独立存在,互不影响。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    original := "Hello, World!"
    cloned := strings.Clone(original)

    fmt.Println("原始:", original)
    fmt.Println("克隆:", cloned)
    fmt.Println("相等?", original == cloned) // true

    // 空字符串克隆
    empty := ""
    clonedEmpty := strings.Clone(empty)
    fmt.Printf("空串克隆: %q\n", clonedEmpty) // ""

    // nil 字符串克隆
    var nilStr *string
    if nilStr != nil {
        fmt.Println(*nilStr)
    }
    // strings.Clone 不接受 *string,这里只是说明行为

    // 实战:函数返回独立副本
    func process(input string) string {
        // 克隆输入,防止外部修改
        working := strings.Clone(input)
        working = strings.ToUpper(working)
        return working
    }

    original2 := "hello"
    result := process(original2)
    fmt.Printf("原始: %s, 结果: %s\n", original2, result)
    // 原始不变: hello, 结果: HELLO

    // 实战:存储独立副本到 map(map 的 key 必须是可比较的,string 本身可比较)
    data := map[string]int{}
    key := "user:123"
    data[key] = 100

    // 克隆 key 后使用
    clonedKey := strings.Clone(key)
    data[clonedKey] = 200
    fmt.Printf("原始 key 值: %d, 克隆 key 值: %d\n", data[key], data[clonedKey])
    // 两个都指向同一个 map 条目,因为内容相同

    // 实战:避免子串引用问题
    big := strings.Repeat("x", 1000)
    sub := big[500:] // 子串,共享底层
    clonedSub := strings.Clone(sub) // 独立副本

    fmt.Printf("big 长度: %d, sub 长度: %d, clonedSub 长度: %d\n", len(big), len(sub), len(clonedSub))
}

为什么要 Clone?

1
2
3
4
5
6
7
8
9
// 问题:不克隆可能共享底层
s1 := "hello"
s2 := s1[:3] // "hel",共享底层

// 如果 s1 被 GC 回收,但 s2 还在引用...
// 这在某些边界情况下可能导致问题

// 解决:Clone 创建独立副本
s2Clone := strings.Clone(s1[:3]) // 完全独立的字符串

一句话总结: Clone 就是"买断版权"——复制一个完全属于自己的版本,不共享底层。


11.35 strings.Cut:切割字符串为两部分(Go 1.18+)

专业解释: Cut 将字符串 s 按 sep 切割成两部分,返回 before(分隔符前的部分)、after(分隔符后的部分)和 found(是否找到分隔符)。如果未找到 sep,found 为 false,before 为 s,after 为空。

通俗理解: Cut 就是"一刀切"——告诉你切点前后分别是什么,就像切蛋糕一样利落。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本用法
    before, after, found := strings.Cut("hello,world", ",")
    fmt.Printf("before: %q, after: %q, found: %v\n", before, after, found)
    // before: "hello", after: "world", found: true

    // 未找到分隔符
    before, after, found = strings.Cut("hello", ",")
    fmt.Printf("before: %q, after: %q, found: %v\n", before, after, found)
    // before: "hello", after: "", found: false

    // 空分隔符(Go 1.20+)
    before, after, found = strings.Cut("hello", "")
    fmt.Printf("before: %q, after: %q, found: %v\n", before, after, found)
    // before: "", after: "hello", found: true

    // 第一个分隔符
    before, after, found = strings.Cut("a=b=c", "=")
    fmt.Printf("before: %q, after: %q, found: %v\n", before, after, found)
    // before: "a", after: "b=c", found: true(只在第一个 = 处切开)

    // 实战:解析键值对
    func parseKV(s string) (key, value string, ok bool) {
        key, value, ok = strings.Cut(s, "=")
        return
    }

    key, value, ok := parseKV("name=张三")
    fmt.Printf("key=%q, value=%q, ok=%v\n", key, value, ok)
    // key="name", value="张三", ok=true

    // 实战:提取路径和文件名
    fullPath := "/home/user/document.txt"
    dir, file, found := strings.Cut(fullPath, "/")
    fmt.Printf("dir: %q, file: %q, found: %v\n", dir, file, found)
    // dir: "", file: "home/user/document.txt", found: true

    // 实战:去除协议
    url := "https://example.com"
    proto, rest, found := strings.Cut(url, "://")
    fmt.Printf("协议: %s, 剩余: %s\n", proto, rest)
    // 协议: https, 剩余: example.com

    // 实战:处理多个分隔符
    data := "key1=value1;key2=value2"
    for {
        pair, rest, found := strings.Cut(data, ";")
        if !found {
            break
        }
        k, v, _ := strings.Cut(pair, "=")
        fmt.Printf("  %s = %s\n", k, v)
        data = rest
    }
    //   key1 = value1
    //   key2 = value2
}

Cut 执行示意:

输入: "hello,world"
分隔符: ","

结果:
before: "hello"    ← 逗号左边
after:  "world"    ← 逗号右边
found:  true

未找到:
输入: "hello"
分隔符: ","

结果:
before: "hello"    ← 原字符串
after:  ""         ← 空
found:  false

一句话总结: Cut 就是"精准切割刀"——告诉你切点前后是什么,简单又直接。


11.36 regexp.Split:按正则分割字符串

专业解释: regexp.Split 将字符串按照正则表达式匹配的内容分割,返回字符串切片。如果正则表达式不匹配输入,Split 返回只包含原字符串的单元素切片。

通俗理解: Split 是按固定字符串分割,Split(正则版)是按"符合某种规律"的内容分割——更灵活,更强大。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
    "fmt"
    "regexp"
)

func main() {
    // 基本用法:按数字分割
    re := regexp.MustCompile(`\d+`)
    parts := re.Split("a1b2c3d", -1)
    fmt.Printf("按数字分割: %q\n", parts) // ["a" "b" "c" "d"]

    // 按空白字符分割(一个或多个)
    re2 := regexp.MustCompile(`\s+`)
    text := "hello   world\n\nfoo\tbar"
    parts2 := re2.Split(text, -1)
    fmt.Printf("按空白分割: %q\n", parts2) // ["hello" "world" "foo" "bar"]

    // 限制分割次数
    parts3 := re.Split("a1b2c3d", 2)
    fmt.Printf("限制 2 次: %q\n", parts3) // ["a" "b2c3d"]

    // 不匹配时返回原字符串
    re3 := regexp.MustCompile(`,`)
    parts4 := re3.Split("hello world", -1)
    fmt.Printf("无匹配: %q\n", parts4) // ["hello world"]

    // 按多种分隔符
    re4 := regexp.MustCompile(`[,;|]`)
    parts5 := re4.Split("a,b;c|d", -1)
    fmt.Printf("多种分隔符: %q\n", parts5) // ["a" "b" "c" "d"]

    // 实战:解析日志时间戳
    logLine := "2024-01-15 10:30:45 INFO Server started"
    reLog := regexp.MustCompile(`\s+`)
    logParts := reLog.Split(logLine, -1)
    fmt.Printf("日志分段: %q\n", logParts)
    // ["2024-01-15" "10:30:45" "INFO" "Server" "started"]

    // 实战:提取驼峰命名
    camelCase := "getUserNameByID"
    reCamel := regexp.MustCompile(`([A-Z])`)
    parts6 := reCamel.Split(camelCase, -1)
    fmt.Printf("驼峰分割: %q\n", parts6) // ["get" "ser" "ame" "y" "D"]
    // 更实用的方式:
    parts7 := reCamel.Split(camelCase, -1)
    fmt.Printf("驼峰分割(简化): %q\n", parts7) // 同上
}

Split vs regexp.Split:

函数分隔依据示例
strings.Split(s, “,”)固定字符串“a,b,c” → [“a”,“b”,“c”]
regexp.Split(re, s)正则表达式“a1b2c” → [“a”,“b”,“c”](按数字切)

一句话总结: regexp.Split 就是"智能切割工"——不仅知道在哪里切,还知道为什么要切(符合某种规律才切)。


11.37 字符串拼接性能排序

专业解释: 在 Go 中,不同的字符串拼接方式有不同的性能特点。从快到慢大致排序为:strings.Builder > strings.Join > bytes.Buffer > fmt.Sprintf > + 拼接。

通俗理解: 拼接字符串就像吃饭——用筷子(+)慢,用勺子(Builder)快,用自助餐(预分配)更快。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
    "bytes"
    "fmt"
    "strings"
    "testing"
)

func main() {
    // 本节主要讲原理,真实性能请用 go test -bench 测试

    // 方式 1: + 拼接(最慢,每次都创建新字符串)
    s := ""
    for i := 0; i < 100; i++ {
        s += "item"
    }
    fmt.Println("+ 拼接:", len(s)) // 400

    // 方式 2: fmt.Sprintf(最慢,格式化开销大)
    s2 := ""
    for i := 0; i < 100; i++ {
        s2 = fmt.Sprintf("%s%s", s2, "item")
    }
    fmt.Println("fmt.Sprintf:", len(s2))

    // 方式 3: strings.Builder(最快,推荐)
    var builder strings.Builder
    for i := 0; i < 100; i++ {
        builder.WriteString("item")
    }
    fmt.Println("Builder:", builder.Len())

    // 方式 4: strings.Join(对于数组拼接很快)
    parts := make([]string, 100)
    for i := range parts {
        parts[i] = "item"
    }
    joined := strings.Join(parts, "")
    fmt.Println("Join:", len(joined))

    // 方式 5: bytes.Buffer
    var buf bytes.Buffer
    for i := 0; i < 100; i++ {
        buf.WriteString("item")
    }
    fmt.Println("bytes.Buffer:", buf.Len())

    // 性能对比总结
    fmt.Println(`
    性能排序(从快到慢):
    1. strings.Builder + WriteString  ← 大量拼接首选
    2. strings.Join                   ← 已知数组时很快
    3. bytes.Buffer + WriteString     ← 接近 Builder
    4. + 拼接                         ← 简单场景可用
    5. fmt.Sprintf                    ← 格式化时用,不适合纯拼接

    选择建议:
    - 10 次以内 + 拼接可接受
    - 10-100 次用 strings.Builder
    - 已知数组用 strings.Join
    - 需要字节操作时用 bytes.Buffer
    `)

    // 实际 benchmark 示意(需要 go test)
    _ = testing.Benchmark
}

性能原理:

+ 拼接: s += "x"
  每次执行: 读取 s → 分配新内存 → 复制 s + "x" → 更新 s
  O(n²) 复杂度

strings.Builder:
  预分配 buffer
  每次 WriteString: 直接追加到 buffer
  O(n) 复杂度

mermaid:

graph TD
    A["字符串拼接方式"] --> B["+ 拼接"]
    A --> C["fmt.Sprintf"]
    A --> D["strings.Join"]
    A --> E["strings.Builder"]
    A --> F["bytes.Buffer"]

    B --> G["最慢 O(n²)"]
    C --> G
    D --> H["快"]
    E --> I["最快 O(n)"]
    F --> H

    style E fill:#90EE90
    style D fill:#98FB98
    style B fill:#FFB6C1
    style C fill:#FFB6C1

一句话总结: 选对拼接方式,性能提升百倍——Builder 是王道,+ 是万不得已。


本章小结

核心概念回顾:

  1. 不可变性:Go 字符串是不可变的字节序列,所有"修改"操作都返回新字符串
  2. UTF-8 编码:Go 字符串默认是 UTF-8 编码,处理中文需要特别注意字节和字符的区别
  3. Builder 模式:频繁拼接时使用 strings.Builder+ 拼接性能高得多

函数分类速查:

类别函数
查找/包含Contains, ContainsAny, ContainsRune, Index, LastIndex, IndexByte, IndexRune, IndexFunc, LastIndexAny
比较EqualFold, Compare
分割Split, SplitAfter, SplitN, Fields, FieldsFunc
拼接Join, Repeat, Builder
替换Replace, ReplaceAll, Map
修剪Trim, TrimLeft, TrimRight, TrimSpace, TrimPrefix, TrimSuffix, TrimFunc
大小写ToLower, ToUpper, ToLowerSpecial, ToUpperSpecial, Title, ToTitle
计数Count
前缀/后缀HasPrefix, HasSuffix
ReaderNewReader, Read, Seek, Size, WriteTo
工具Clone, Cut

实战建议:

  • 频繁字符串拼接用 strings.Builder
  • 分割字符串优先用 strings.Splitstrings.Fields
  • 大小写不敏感比较用 strings.EqualFold
  • 中文处理注意使用 rune 而非直接按字节操作
  • Go 1.18+ 推荐使用 strings.Clonestrings.Cut

性能提示:

  • + 拼接:简单场景,次数少时可用
  • strings.Builder:大量拼接首选
  • strings.Join:已知数组时很快
  • fmt.Sprintf:格式化时用,不适合纯拼接

本章完!第 12 章再见!

最后修改 March 30, 2026: 新增 Go 标准库基础 教程 (acbc3f6)