Go 从 0 到精通 · 第 21 课:字符串处理与常用工具库

学习定位:这是整套 Go 教程的第 21 课。
前置要求:已经完成第 20 课,掌握了时间处理。需要熟悉第 11 课的字符串、byterune 基础。
本课目标:掌握 stringsstrconvunicode 三个包的常用函数,能高效完成文本清洗、拼接、拆分、查找、替换和类型转换任务。


1. 本课你要解决的核心问题

在第 11 课你已经理解了字符串的底层是 []byte,中文用 rune 处理。但在实际开发中,你面对的往往不是"遍历字符串",而是:

  • 用户输入的文本里有前后空格,要去掉
  • 从配置文件读到的字符串要拆分成列表
  • 要判断一个字符串是否包含某个子串
  • 要把数字转成字符串、字符串转回数字
  • 要判断一个字符是中文、数字还是字母

这些操作如果自己用 for 循环写,既繁琐又容易出错。Go 的标准库已经帮你做好了。

你需要搞明白以下问题:

  • strings 包有哪些常用函数,怎么用
  • strconv 包怎么在字符串和数字之间转换
  • unicode 包怎么判断字符类型
  • 这些函数组合起来怎么处理真实场景

学完这一课,你就能高效处理各种文本数据了。


2. strings 包——字符串操作瑞士军刀

strings 包是 Go 中最常用的字符串工具库。它的函数非常多,我们按功能分类逐个讲解。

2.1 大小写转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"strings"
)

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

fmt.Println(strings.ToUpper(s)) // HELLO, GO WORLD!
fmt.Println(strings.ToLower(s)) // hello, go world!
fmt.Println(strings.Title(s)) // Hello, Go World!(每个单词首字母大写)
fmt.Println(strings.ToTitle(s)) // HELLO, GO WORLD!(每个字符转为 title case)
}

Title vs ToTitle 的区别(很多人搞混):

  • strings.Title("hello world")"Hello World"(每个单词首字母大写)
  • strings.ToTitle("hello world")"HELLO WORLD"(每个字符转为 Unicode title case)

注意:Go 1.18 之后 strings.Title 已被标记为 deprecated,推荐用 golang.org/x/text/cases 包。但在学习阶段了解即可。

2.2 去除空白

1
2
3
4
5
s := "   Hello, Go!   "

fmt.Println(strings.TrimSpace(s)) // "Hello, Go!"(去掉前后空白)
fmt.Println(strings.TrimLeft(s, " ")) // "Hello, Go! "(只去掉左边)
fmt.Println(strings.TrimRight(s, " ")) // " Hello, Go!"(只去掉右边)

Trim 去除指定字符

1
2
3
4
5
6
7
8
9
10
11
s := "###Hello###"
fmt.Println(strings.Trim(s, "#")) // "Hello"

// TrimLeft 和 TrimRight 分别处理左右
s2 := "00012300"
fmt.Println(strings.TrimLeft(s2, "0")) // "12300"
fmt.Println(strings.TrimRight(s2, "0")) // "000123"

// TrimPrefix 和 TrimSuffix(只能去掉完整前缀/后缀,不是逐字符)
fmt.Println(strings.TrimPrefix("HelloWorld", "Hello")) // "World"
fmt.Println(strings.TrimSuffix("HelloWorld", "World")) // "Hello"

实战:清理用户输入

1
2
3
4
5
6
7
8
9
func cleanInput(input string) string {
// 去掉前后空白 → 转小写 → 去掉首尾引号
s := strings.TrimSpace(input)
s = strings.ToLower(s)
s = strings.Trim(s, "\"'")
return s
}

fmt.Println(cleanInput(" \"Hello World\" ")) // hello world

2.3 查找

判断是否包含

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
s := "Hello, Go World!"

fmt.Println(strings.Contains(s, "Go")) // true
fmt.Println(strings.Contains(s, "Python")) // false

// 不区分大小写
fmt.Println(strings.Contains(strings.ToLower(s), "go")) // true

// 检查是否包含任意一个字符
fmt.Println(strings.ContainsAny(s, "xyz")) // false
fmt.Println(strings.ContainsAny(s, "Go")) // true

// 检查是否包含 rune
fmt.Println(strings.ContainsRune(s, '世')) // false(没有中文)
fmt.Println(strings.ContainsRune("你好", '你')) // true

判断开头和结尾

1
2
3
4
5
s := "config.yaml"

fmt.Println(strings.HasPrefix(s, "config")) // true
fmt.Println(strings.HasSuffix(s, ".yaml")) // true
fmt.Println(strings.HasSuffix(s, ".json")) // false

实战:根据文件扩展名做不同处理

1
2
3
4
5
6
7
8
9
10
11
12
func processFile(filename string) {
switch {
case strings.HasSuffix(filename, ".go"):
fmt.Println("Go 源文件")
case strings.HasSuffix(filename, ".md"):
fmt.Println("Markdown 文档")
case strings.HasSuffix(filename, ".json"):
fmt.Println("JSON 配置")
default:
fmt.Println("未知类型")
}
}

查找位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
s := "hello world hello go"

// 返回子串第一次出现的位置,找不到返回 -1
fmt.Println(strings.Index(s, "hello")) // 0
fmt.Println(strings.Index(s, "go")) // 17
fmt.Println(strings.Index(s, "python")) // -1

// 返回子串最后一次出现的位置
fmt.Println(strings.LastIndex(s, "hello")) // 12

// IndexAny:返回任意一个字符第一次出现的位置
fmt.Println(strings.IndexAny("hello", "aeiou")) // 1('e' 的位置)

// Count:统计子串出现次数
fmt.Println(strings.Count(s, "hello")) // 2

2.4 替换

1
2
3
4
5
6
7
8
9
10
11
s := "Hello, Go! Go is great. Go!"

// 替换:前 n 个,-1 表示全部替换
fmt.Println(strings.Replace(s, "Go", "Python", 1))
// "Hello, Python! Go is great. Go!"(只替换第 1 个)

fmt.Println(strings.Replace(s, "Go", "Python", -1))
// "Hello, Python! Python is great. Python!"(全部替换)

// ReplaceAll(Go 1.12+):等价于 Replace(..., -1)
fmt.Println(strings.ReplaceAll(s, "Go", "Python"))

实战:模板替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func renderTemplate(template string, data map[string]string) string {
result := template
for key, value := range data {
placeholder := "{{" + key + "}}"
result = strings.ReplaceAll(result, placeholder, value)
}
return result
}

tmpl := "你好,{{name}}!你今年 {{age}} 岁了。"
data := map[string]string{
"name": "小明",
"age": "20",
}
fmt.Println(renderTemplate(tmpl, data))
// 输出:你好,小明!你今年 20 岁了。

2.5 拆分

Split:按分隔符拆分

1
2
3
4
5
s := "apple,banana,cherry"

parts := strings.Split(s, ",")
fmt.Println(parts) // [apple banana cherry]
fmt.Println(len(parts)) // 3

SplitN:限制拆分数量

1
2
3
4
5
6
7
8
9
10
11
s := "user:password:host:port"

// 只拆分前 2 个,剩余的保留
parts := strings.SplitN(s, ":", 2)
fmt.Println(parts) // [user password:host:port]

// 常见用法:解析 "key=value" 格式的配置
line := "name=Alice Smith,25"
kv := strings.SplitN(line, "=", 2)
fmt.Println(kv[0]) // "name"
fmt.Println(kv[1]) // "Alice Smith,25"

Fields:按空白拆分(连续空白算一个)

1
2
3
4
5
6
7
8
9
s := "  hello   world   go  "

fields := strings.Fields(s)
fmt.Println(fields) // [hello world go]
fmt.Println(len(fields)) // 3

// 如果用 Split 按空格拆分,会有空元素
parts := strings.Split(s, " ")
fmt.Println(parts) // [ hello world go ](有空串)

实战:解析命令行参数

1
2
3
input := "  git   commit  -m  \"fix bug\"  "
args := strings.Fields(input)
fmt.Println(args) // [git commit -m "fix bug"]

SplitAfter:保留分隔符

1
2
3
4
5
6
7
s := "a,b,c"

// Split 去掉分隔符
fmt.Println(strings.Split(s, ",")) // [a b c]

// SplitAfter 保留分隔符在前一个元素的末尾
fmt.Println(strings.SplitAfter(s, ",")) // [a, b, c]

2.6 拼接

Join:最高效的拼接方式

1
2
3
4
parts := []string{"apple", "banana", "cherry"}

s := strings.Join(parts, ", ")
fmt.Println(s) // apple, banana, cherry

Join 是拼接字符串列表的首选方式,比循环 += 高效得多(因为 += 每次都分配新内存)。

各种拼接方式的性能对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式一:+= 拼接(最慢,每次分配新内存)
result := ""
for _, part := range parts {
result += part + ", "
}

// 方式二:Join(推荐,一次分配)
result := strings.Join(parts, ", ")

// 方式三:strings.Builder(更灵活,拼接大量字符串时推荐)
var builder strings.Builder
for _, part := range parts {
builder.WriteString(part)
builder.WriteString(", ")
}
result := builder.String()

strings.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
package main

import (
"fmt"
"strings"
)

func main() {
var builder strings.Builder

// 可以预分配容量,减少内存重分配
builder.Grow(100)

builder.WriteString("Hello")
builder.WriteByte(' ')
builder.WriteString("World")
builder.WriteRune('!')

// 获取结果
result := builder.String()
fmt.Println(result) // Hello World!

// 重置
builder.Reset()
builder.WriteString("新的内容")
fmt.Println(builder.String()) // 新的内容
}

2.7 重复

1
2
3
4
5
fmt.Println(strings.Repeat("=-", 10))
// =-=-=-=-=-=-=-=-=-=-

fmt.Println(strings.Repeat("Go ", 3))
// Go Go Go

2.8 strings 包速查表

函数 用途 示例
Contains(s, substr) 是否包含 Contains("hello", "ell")true
HasPrefix(s, prefix) 是否以此开头 HasPrefix("hello", "hel")true
HasSuffix(s, suffix) 是否以此结尾 HasSuffix("file.go", ".go")true
Index(s, substr) 第一次出现的位置 Index("abcabc", "bc")1
Count(s, substr) 出现次数 Count("aaa", "aa")1
ReplaceAll(s, old, new) 全部替换 ReplaceAll("aaa", "a", "b")"bbb"
Split(s, sep) 按分隔符拆分 Split("a,b", ",")["a","b"]
Fields(s) 按空白拆分 Fields(" a b ")["a","b"]
Join(slice, sep) 拼接 Join(["a","b"], "-")"a-b"
Trim(s, cutset) 去除首尾指定字符 Trim("##hi#", "#")"hi"
TrimSpace(s) 去除首尾空白 TrimSpace(" hi ")"hi"
ToUpper(s) 转大写 ToUpper("hi")"HI"
ToLower(s) 转小写 ToLower("HI")"hi"
Repeat(s, count) 重复 Repeat("Go", 3)"GoGoGo"

3. strconv 包——字符串与数字互转

3.1 整数转字符串

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

import (
"fmt"
"strconv"
)

func main() {
// Itoa:int to ASCII 的缩写
s := strconv.Itoa(42)
fmt.Println(s) // "42"
fmt.Println(s + "岁") // "42岁"

// FormatInt:指定进制
fmt.Println(strconv.FormatInt(255, 10)) // "255"(十进制)
fmt.Println(strconv.FormatInt(255, 16)) // "ff"(十六进制)
fmt.Println(strconv.FormatInt(255, 2)) // "11111111"(二进制)
fmt.Println(strconv.FormatInt(255, 8)) // "377"(八进制)

// FormatUint:无符号版本
fmt.Println(strconv.FormatUint(255, 16)) // "ff"
}

3.2 浮点数转字符串

1
2
3
4
5
6
7
8
9
// FormatFloat:格式 + 精度 + 位宽
// 'f':小数形式,'e':科学计数法,'g':自动选择
fmt.Println(strconv.FormatFloat(3.14159, 'f', 2, 64)) // "3.14"(保留2位)
fmt.Println(strconv.FormatFloat(3.14159, 'f', -1, 64)) // "3.14159"(不截断)
fmt.Println(strconv.FormatFloat(3.14159, 'e', -1, 64)) // "3.14159e+00"
fmt.Println(strconv.FormatFloat(1e10, 'f', -1, 64)) // "10000000000"

// Ftoa:简写
fmt.Println(strconv.FormatFloat(3.14, 'f', 2, 64)) // "3.14"

3.3 布尔值转字符串

1
2
fmt.Println(strconv.FormatBool(true))   // "true"
fmt.Println(strconv.FormatBool(false)) // "false"

3.4 字符串转整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Atoi:ASCII to int
n, err := strconv.Atoi("42")
if err != nil {
fmt.Println("转换失败:", err)
return
}
fmt.Println(n) // 42
fmt.Printf("类型:%T\n", n) // int

// ParseInt:指定进制和位宽
// ParseInt(str, base, bitSize)
// base: 0=自动检测, 10=十进制, 16=十六进制, ...
// bitSize: 8=int8, 16=int16, 32=int32, 64=int64
n64, _ := strconv.ParseInt("ff", 16, 64)
fmt.Println(n64) // 255

n64, _ = strconv.ParseInt("1010", 2, 64)
fmt.Println(n64) // 10

// ParseUint:无符号版本
u64, _ := strconv.ParseUint("255", 10, 64)
fmt.Println(u64) // 255

3.5 字符串转浮点数

1
2
3
4
5
6
7
8
9
10
11
f, err := strconv.ParseFloat("3.14159", 64)
if err != nil {
fmt.Println("转换失败:", err)
return
}
fmt.Println(f) // 3.14159
fmt.Printf("%T\n", f) // float64

// 科学计数法也能解析
f, _ = strconv.ParseFloat("1.5e10", 64)
fmt.Println(f) // 15000000000

3.6 字符串转布尔值

1
2
3
4
5
6
7
8
9
10
11
// 接受 "1", "t", "T", "true", "TRUE", "True"
b, _ := strconv.ParseBool("true")
fmt.Println(b) // true

// 接受 "0", "f", "F", "false", "FALSE", "False"
b, _ = strconv.ParseBool("false")
fmt.Println(b) // false

// 其他值会报错
_, err := strconv.ParseBool("yes")
fmt.Println(err) // strconv.ParseBool: parsing "yes": invalid syntax

3.7 错误处理

strconv 的解析函数在转换失败时会返回一个特殊的错误类型 *strconv.NumError

1
2
3
4
5
6
7
8
9
n, err := strconv.Atoi("abc")
if err != nil {
// 检查是否是数值范围错误
if numErr, ok := err.(*strconv.NumError); ok {
fmt.Println("错误操作:", numErr.Func) // "strconv.Atoi"
fmt.Println("错误输入:", numErr.Num) // "abc"
fmt.Println("错误类型:", numErr.Err) // "invalid syntax"
}
}

3.8 strconv 包速查表

函数 方向 示例
Itoa(n) int → string Itoa(42)"42"
Atoi(s) string → int Atoi("42")42, nil
FormatInt(n, base) int64 → string FormatInt(255, 16)"ff"
ParseInt(s, base, bits) string → int64 ParseInt("ff", 16, 64)255
FormatFloat(f, fmt, prec, bits) float64 → string FormatFloat(3.14, 'f', 2, 64)"3.14"
ParseFloat(s, bits) string → float64 ParseFloat("3.14", 64)3.14
FormatBool(b) bool → string FormatBool(true)"true"
ParseBool(s) string → bool ParseBool("true")true
Quote(s) string → 带引号的字符串 Quote("hi")"\"hi\""
Unquote(s) 带引号的字符串 → string Unquote("\"hi\"")"hi"

4. unicode 包——字符类型判断

4.1 判断字符类型

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

import (
"fmt"
"unicode"
)

func main() {
chars := []rune{'A', 'z', '3', '中', ' ', '\t', '\n', '!', '9'}

for _, ch := range chars {
fmt.Printf("字符 '%c':\n", ch)
fmt.Printf(" 字母: %v\n", unicode.IsLetter(ch))
fmt.Printf(" 数字: %v\n", unicode.IsDigit(ch))
fmt.Printf(" 数字(含全角): %v\n", unicode.IsNumber(ch))
fmt.Printf(" 空白: %v\n", unicode.IsSpace(ch))
fmt.Printf(" 大写: %v\n", unicode.IsUpper(ch))
fmt.Printf(" 小写: %v\n", unicode.IsLower(ch))
fmt.Printf(" 标点: %v\n", unicode.IsPunct(ch))
fmt.Printf(" 符号: %v\n", unicode.IsSymbol(ch))
fmt.Println()
}
}

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
字符 'A':
字母: true
数字: false
大写: true
小写: false

字符 '中':
字母: true
数字: false
大写: false
小写: false

字符 '9':
字母: false
数字: true(全角数字也算数字)

4.2 字符大小写转换

1
2
3
4
5
6
7
8
9
ch := 'a'
fmt.Printf("%c → %c\n", ch, unicode.ToUpper(ch)) // a → A

ch = 'B'
fmt.Printf("%c → %c\n", ch, unicode.ToLower(ch)) // B → b

// 中文没有大小写之分
ch = '中'
fmt.Printf("%c → %c\n", ch, unicode.ToUpper(ch)) // 中 → 中

4.3 实战:统计文本中各类字符的数量

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
package main

import (
"fmt"
"unicode"
)

func countChars(s string) (letters, digits, spaces, others int) {
for _, ch := range s {
switch {
case unicode.IsLetter(ch):
letters++
case unicode.IsDigit(ch):
digits++
case unicode.IsSpace(ch):
spaces++
default:
others++
}
}
return
}

func main() {
text := "Hello 你好 123 Go! 世界"
l, d, s, o := countChars(text)
fmt.Printf("字母: %d, 数字: %d, 空白: %d, 其他: %d\n", l, d, s, o)
// 字母: 7, 数字: 3, 空白: 5, 其他: 1
}

5. 实战综合示例

5.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
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package main

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

// CleanUsername 清洗用户名
func CleanUsername(input string) string {
s := strings.TrimSpace(input)
s = strings.ToLower(s)
return s
}

// ValidatePassword 校验密码强度
// 要求:至少8位,包含大写、小写、数字
func ValidatePassword(pwd string) (bool, []string) {
var problems []string

if len(pwd) < 8 {
problems = append(problems, "密码长度不足8位")
}

hasUpper, hasLower, hasDigit := false, false, false
for _, ch := range pwd {
switch {
case unicode.IsUpper(ch):
hasUpper = true
case unicode.IsLower(ch):
hasLower = true
case unicode.IsDigit(ch):
hasDigit = true
}
}

if !hasUpper {
problems = append(problems, "缺少大写字母")
}
if !hasLower {
problems = append(problems, "缺少小写字母")
}
if !hasDigit {
problems = append(problems, "缺少数字")
}

return len(problems) == 0, problems
}

// ParseAge 解析年龄输入
func ParseAge(input string) (int, error) {
s := strings.TrimSpace(input)
age, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("无法解析年龄: %w", err)
}
if age < 0 || age > 200 {
return 0, fmt.Errorf("年龄 %d 不在合理范围内", age)
}
return age, nil
}

func main() {
// 测试用户名清洗
usernames := []string{" Alice ", "BOB", " Charlie "}
for _, u := range usernames {
fmt.Printf("输入: [%s] → 清洗后: [%s]\n", u, CleanUsername(u))
}

fmt.Println()

// 测试密码校验
passwords := []string{"abc", "abcdefgh", "Abcdefg1", "abcdefg1", "ABCDEFG1"}
for _, pwd := range passwords {
ok, problems := ValidatePassword(pwd)
if ok {
fmt.Printf("密码 [%s]: 通过\n", pwd)
} else {
fmt.Printf("密码 [%s]: 不通过 - %s\n", pwd, strings.Join(problems, "、"))
}
}

fmt.Println()

// 测试年龄解析
ageInputs := []string{"25", " 30 ", "abc", "200", "-5"}
for _, input := range ageInputs {
age, err := ParseAge(input)
if err != nil {
fmt.Printf("输入 [%s]: 错误 - %s\n", input, err)
} else {
fmt.Printf("输入 [%s]: 年龄 %d\n", input, age)
}
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输入: [  Alice  ] → 清洗后: [alice]
输入: [BOB] → 清洗后: [bob]
输入: [ Charlie ] → 清洗后: [charlie]

密码 [abc]: 不通过 - 密码长度不足8位、缺少大写字母、缺少数字
密码 [abcdefgh]: 不通过 - 缺少大写字母、缺少数字
密码 [Abcdefg1]: 通过
密码 [abcdefg1]: 不通过 - 缺少大写字母
密码 [ABCDEFG1]: 不通过 - 缺少小写字母

输入 [25]: 年龄 25
输入 [ 30 ]: 年龄 30
输入 [abc]: 错误 - 无法解析年龄: strconv.Atoi: parsing "abc": invalid syntax
输入 [200]: 错误 - 年龄 200 不在合理范围内
输入 [-5]: 错误 - 年龄 -5 不在合理范围内

5.2 CSV 行解析器

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
package main

import (
"fmt"
"strings"
)

// ParseCSVLine 解析一行 CSV(简化版,不处理引号内的逗号)
func ParseCSVLine(line string) []string {
line = strings.TrimSpace(line)
if line == "" {
return nil
}
fields := strings.Split(line, ",")

// 清洗每个字段
for i, f := range fields {
fields[i] = strings.TrimSpace(f)
}
return fields
}

func main() {
csvData := `
name,age,city
Alice,25,Beijing
Bob,30,Shanghai
Charlie,28,Guangzhou
`

lines := strings.Split(strings.TrimSpace(csvData), "\n")

// 第一行是表头
headers := ParseCSVLine(lines[0])
fmt.Println("表头:", headers)

// 解析数据行
fmt.Println()
for i, line := range lines[1:] {
fields := ParseCSVLine(line)
if len(fields) != len(headers) {
fmt.Printf("第 %d 行字段数量不匹配\n", i+2)
continue
}
// 用 Builder 拼接输出
var sb strings.Builder
for j, field := range fields {
if j > 0 {
sb.WriteString(" | ")
}
sb.WriteString(headers[j])
sb.WriteString(": ")
sb.WriteString(field)
}
fmt.Println(sb.String())
}
}

输出:

1
2
3
4
5
表头: [name age city]

name: Alice | age: 25 | city: Beijing
name: Bob | age: 30 | city: Shanghai
name: Charlie | age: 28 | city: Guangzhou

5.3 文本统计工具

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
82
83
84
85
86
87
88
89
90
91
92
93
package main

import (
"bufio"
"fmt"
"os"
"strings"
"unicode"
)

func analyzeText(text string) {
runes := []rune(text)
fmt.Printf("总字符数(rune): %d\n", len(runes))
fmt.Printf("总字节数(byte): %d\n", len(text))

// 统计各类字符
var chinese, letters, digits, spaces, punctuation, other int
for _, r := range runes {
switch {
case unicode.Is(unicode.Han, r):
chinese++
case unicode.IsLetter(r):
letters++
case unicode.IsDigit(r):
digits++
case unicode.IsSpace(r):
spaces++
case unicode.IsPunct(r):
punctuation++
default:
other++
}
}

fmt.Printf("中文字符: %d\n", chinese)
fmt.Printf("其他字母: %d\n", letters)
fmt.Printf("数字: %d\n", digits)
fmt.Printf("空白: %d\n", spaces)
fmt.Printf("标点: %d\n", punctuation)
fmt.Printf("其他: %d\n", other)

// 统计行数
lines := strings.Split(text, "\n")
fmt.Printf("总行数: %d\n", len(lines))

// 统计单词数(按空白分隔)
words := strings.Fields(text)
fmt.Printf("单词/词组数: %d\n", len(words))

// 统计词频(简化处理)
freq := make(map[string]int)
for _, w := range words {
w = strings.ToLower(strings.Trim(w, ".,;:!?\"'()[]{}"))
if w != "" {
freq[w]++
}
}

fmt.Println("\n高频词 TOP 5:")
// 简单遍历(实际应用需要排序)
for word, count := range freq {
if count >= 2 {
fmt.Printf(" %s: %d\n", word, count)
}
}
}

func main() {
// 从标准输入读取多行文本,空行结束
fmt.Println("请输入文本(空行结束):")
scanner := bufio.NewScanner(os.Stdin)
var lines []string
for scanner.Scan() {
line := scanner.Text()
if line == "" {
break
}
lines = append(lines, line)
}

text := strings.Join(lines, "\n")
if text == "" {
// 没有输入就用示例文本
text = `Go is an open source programming language.
Go makes it easy to build simple, reliable, and efficient software.
Go is expressive, concise, clean, and efficient.
Go is fast, statically typed, compiled language.
Go 是一门开源的编程语言。`
}

fmt.Println("\n===== 文本分析结果 =====")
analyzeText(text)
}

6. fmt 包中与字符串相关的技巧

6.1 fmt.Sprintf:格式化字符串

1
2
3
4
5
6
7
name := "Alice"
age := 25
score := 95.5

// 各种格式化动词
s := fmt.Sprintf("姓名: %s, 年龄: %d, 分数: %.1f", name, age, score)
fmt.Println(s) // 姓名: Alice, 年龄: 25, 分数: 95.5

常用格式化动词:

动词 含义 示例
%s 字符串 "hello"
%d 十进制整数 42
%f 浮点数 3.14159
%.2f 保留2位小数 3.14
%t 布尔值 true
%v 默认格式 任意值
%T 类型 int
%x 十六进制 ff
%% 百分号 %

6.2 fmt.Errorf:格式化错误信息

1
2
err := fmt.Errorf("文件 %s 不存在(大小: %d)", "data.txt", 0)
fmt.Println(err) // 文件 data.txt 不存在(大小: 0)

6.3 fmt.Sprint 系列

1
2
3
4
5
6
7
8
// Sprint:连接多个参数为字符串(无分隔符)
s := fmt.Sprint("Hello", " ", "World")

// Sprintln:连接并加空格和换行
s = fmt.Sprintln("Hello", "World")

// Sprintf:格式化
s = fmt.Sprintf("Hello, %s!", "Go")

7. 常见坑总结

7.1 strings.Split 在空字符串上的行为

1
2
3
4
5
6
7
8
9
10
11
// 空字符串按逗号拆分,得到一个包含空串的切片,不是空切片!
parts := strings.Split("", ",")
fmt.Println(len(parts)) // 1(不是 0)
fmt.Println(parts[0]) // ""

// 正确做法:先检查
if s == "" {
parts = []string{}
} else {
parts = strings.Split(s, ",")
}

7.2 strings.Replace 的 n 参数

1
2
3
4
5
6
7
8
9
10
s := "aaa"

// n=0 不替换任何内容!(不是"替换全部")
fmt.Println(strings.Replace(s, "a", "b", 0)) // aaa

// n=-1 才是全部替换
fmt.Println(strings.Replace(s, "a", "b", -1)) // bbb

// n=1 只替换第一个
fmt.Println(strings.Replace(s, "a", "b", 1)) // baa

7.3 strconv.Atoi 和中文数字

1
2
3
4
5
6
7
// Atoi 只处理 ASCII 数字
n, err := strconv.Atoi("42")
fmt.Println(n, err) // 42 <nil>

// 中文数字不行
n, err = strconv.Atoi("四十二")
fmt.Println(n, err) // 0 invalid syntax

7.4 strconv.ParseFloat 的精度问题

1
2
3
4
5
6
// 浮点数的精度是有限的
f, _ := strconv.ParseFloat("0.1", 64)
fmt.Printf("%.20f\n", f) // 0.10000000000000000555
// 这不是 strconv 的问题,是浮点数本身的特性

// 需要精确计算时,不要用 float64(后面会学 math/big)

7.5 strings.ToLower / ToUpper 和 Unicode

1
2
3
4
5
6
// 对于纯 ASCII 没问题
fmt.Println(strings.ToLower("HELLO")) // hello

// 但某些 Unicode 字符的大小写转换不是一对一的
// 例如德语 ß 的大写是 SS
fmt.Println(strings.ToUpper("straße")) // STRASSE(不是 STRAßE)

7.6 unicode.IsDigit vs unicode.IsNumber

1
2
3
4
5
6
7
8
9
// IsDigit 只匹配 0-9
fmt.Println(unicode.IsDigit('0')) // true
fmt.Println(unicode.IsDigit('9')) // true(全角也算)
fmt.Println(unicode.IsDigit('²')) // false(上标数字不算)

// IsNumber 范围更广
fmt.Println(unicode.IsNumber('0')) // true
fmt.Println(unicode.IsNumber('²')) // true
fmt.Println(unicode.IsNumber('三')) // true(中文数字也算)

7.7 字符串拼接用 += 而不是 strings.Join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 错误:在循环中用 += 拼接,性能极差
result := ""
for _, s := range slices {
result += s + ","
}
// 每次 += 都分配新的内存,n 个字符串的时间复杂度是 O(n²)

// 正确:用 Join(一次分配)
result := strings.Join(slices, ",")

// 或者用 strings.Builder
var b strings.Builder
for _, s := range slices {
b.WriteString(s)
b.WriteString(",")
}
result := b.String()

7.8 strconv.Itoa 和负数

1
2
3
4
5
6
7
8
9
10
11
// Itoa 可以处理负数
s := strconv.Itoa(-42)
fmt.Println(s) // "-42"

// 但 ParseInt 需要处理负号
n, _ := strconv.ParseInt("-42", 10, 64)
fmt.Println(n) // -42

// ParseUint 不接受负号
n, err := strconv.ParseUint("-42", 10, 64)
fmt.Println(n, err) // 0 invalid syntax

8. 本课练习

练习 1:字符串清洗

要求:

  • 写一个函数,接收用户输入的字符串
  • 去掉前后空白、转为小写
  • 去掉所有标点符号(提示:strings.Map + unicode.IsPunct
  • 返回清洗后的结果

练习 2:简易计算器

要求:

  • 接收一个字符串表达式,如 "123 + 456"
  • 拆分出两个数字和运算符(提示:strings.SplitN + strings.Fields
  • strconv.Atoi 转成数字
  • 计算结果并输出

练习 3:统计文章字数

要求:

  • 写一个函数统计中文文章的字数
  • 中文按字符计数,英文按单词计数
  • 统计有多少个段落(以空行分隔)

练习 4:配置文件解析

要求:

  • 解析 key=value 格式的配置行
  • 跳过注释行(以 # 开头)
  • 跳过空行
  • 去掉值中的前后空白
  • 返回 map[string]string

练习 5:字符串加密(简单版)

要求:

  • 写一个凯撒加密函数:每个字母移动 n 位(如 a→d, b→e
  • unicode.IsLetter 判断是否是字母
  • unicode.IsUpper 判断大小写
  • 非字母字符保持不变

9. 自测题

9.1 概念题

  1. strings.Splitstrings.Fields 有什么区别?
  2. strings.Join 为什么比循环 += 拼接高效?
  3. strconv.Itoastrconv.FormatInt(42, 10) 有什么关系?
  4. strconv.Atoi 能处理中文数字 "四十二" 吗?
  5. unicode.IsDigitunicode.IsNumber 的区别是什么?
  6. strings.Replace 的第三个参数 0-1 分别是什么意思?
  7. strings.BuilderReset 方法有什么用?
  8. strconv.ParseIntbase 参数设为 0 是什么意思?
  9. strings.TrimPrefixstrings.Trim 有什么区别?
  10. 用什么函数可以把字符串中的每个字符按自定义规则映射?

9.2 代码阅读题

预测以下代码的输出:

1
2
3
4
5
6
7
8
9
10
11
func mystery(s string) string {
fields := strings.Fields(s)
for i, f := range fields {
if len(f) > 0 {
fields[i] = strings.ToUpper(string(f[0])) + strings.ToLower(f[1:])
}
}
return strings.Join(fields, " ")
}

fmt.Println(mystery(" hello WORLD go "))
点击查看答案
1
Hello World Go

解释:

  1. strings.Fields 去掉多余空白,拆成 ["hello", "WORLD", "go"]
  2. 每个单词首字母大写 + 其余小写
  3. 用空格 Join 回去

10. 本课总结

这一课你学到了字符串处理的三个核心包。

场景 推荐函数
去掉空白 strings.TrimSpace
大小写转换 strings.ToUpper / ToLower
查找子串 strings.Contains / Index / Count
判断开头/结尾 strings.HasPrefix / HasSuffix
替换 strings.ReplaceAll
拆分 strings.Split(有分隔符)/ strings.Fields(按空白)
拼接 strings.Join(切片)/ strings.Builder(复杂拼接)
整数 ↔ 字符串 strconv.Itoa / strconv.Atoi
浮点数 ↔ 字符串 strconv.FormatFloat / strconv.ParseFloat
布尔 ↔ 字符串 strconv.FormatBool / strconv.ParseBool
判断字符类型 unicode.IsLetter / IsDigit / IsSpace / IsUpper

最重要的三件事:

  1. 字符串拼接用 JoinBuilder,不要在循环里用 +=
  2. 字符串转数字用 strconv.Atoi / ParseFloat,一定要检查错误
  3. 判断字符类型用 unicode 包,不要自己写范围判断

11. 下一课预告

下一课我们学习 JSON 编解码

会重点讲:

  • encoding/json 包的使用
  • 结构体与 JSON 的相互转换
  • 结构体标签(json:"field_name"
  • 处理未知结构的 JSON

学完下一课,你就能处理接口响应、配置文件和数据交换了。