第 14 章:正则表达式——regexp 包

第 14 章:正则表达式——regexp 包

正则表达式,这个让程序员又爱又恨的小东西。爱它,是因为它能用几行代码解决你原本要写几百行才能搞定的问题;恨它,是因为那几行代码你可能得调试一整天。Google 开发的 RE2 引擎为 Go 语言提供了稳定、高效的正则实现——没有那些花里胡哨(反向引用、lookahead)的特性,反而让你的正则更安全、更可预测。

14.1 regexp 包解决什么问题

正则表达式最常见的三大应用场景:验证格式提取内容替换文本。想象一下,你要验证用户输入的邮箱是不是合法、从 HTML 里扣出所有 URL、把"SB"“草泥马"这些敏感词打成马赛克——没有正则的话,这些活儿能让你写到怀疑人生。

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

import (
	"fmt"
	"regexp"
)

func main() {
	// 验证邮箱格式 —— 简单粗暴,但够用
	email := "boss@example.com"
	emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
	fmt.Println("邮箱验证:", emailRegex.MatchString(email)) // 邮箱验证: true

	// 模拟从网页文本中提取 URL
	html := `欢迎访问 <a href="https://golang.org">Go语言官网</a> 和 <a href="https://pkg.go.dev">Go包仓库</a>`
	urlRegex := regexp.MustCompile(`https?://[^\s"'<>]+`)
	urls := urlRegex.FindAllString(html, -1)
	fmt.Println("提取到的URL:", urls) // 提取到的URL: [https://golang.org https://pkg.go.dev]

	// 替换敏感词 —— 互联网冲浪必备技能
	sensitive := "我去你个SB,这草泥马什么玩意儿!"
	sensitiveRegex := regexp.MustCompile(`SB|草泥马|卧槽`)
	Masked := sensitiveRegex.ReplaceAllString(sensitive, "**")
	fmt.Println("脱敏后:", Masked) // 脱敏后: 我去你个**,这**什么玩意儿!
}

专业词汇解释:

  • MatchString:判断字符串是否完全匹配正则表达式(从开头到结尾)
  • FindAllString:找出所有匹配的子串,-1 表示不限制数量
  • ReplaceAllString:把所有匹配项替换成指定字符串

14.2 regexp 核心原理:RE2 引擎

Go 的 regexp 包底层使用的是 Google 开发的 RE2 正则引擎。RE2 的设计哲学是:快速、安全、可并发。它使用有限状态自动机(Finite Automaton)来匹配正则,性能接近手工编写的解析器,而且绝对不会出现正则引发的 ReDoS(正则表达式拒绝服务攻击)漏洞。

不过鱼和熊掌不可兼得,RE2 为了保证这些特性,不支持以下特性:

  • 反向引用(Backreference):例如 \1 引用第一个分组,Go 不支持
  • ** lookahead/lookbehind**:零宽断言,(?=...)(?<=...) 等,Go 不支持
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
	"fmt"
	"regexp"
)

func main() {
	// RE2 保证了确定性的匹配性能
	// 不会出现某些回溯正则那种指数级爆炸

	text := "The quick brown fox jumps over the lazy dog"

	// 简单正则,编译后复用,效率很高
	re := regexp.MustCompile(`\b\w{5}\b`) // 匹配所有5个字母的单词

	words := re.FindAllString(text, -1)
	fmt.Println("5字母单词:", words) // 5字母单词: [quick brown jumps]

	// 编译一次,使用多次 —— 这是 RE2 最佳实践
	for _, s := range []string{"hello", "world", "fox"} {
		fmt.Printf("%s 包含5字母单词: %v\n", s, re.MatchString(s))
	}
}

RE2 工作原理图:

flowchart LR
    A[正则表达式] --> B[解析器 Parse]
    B --> C[抽象语法树 AST]
    C --> D[编译器 Compile]
    D --> E[字节码 Bytecode]
    E --> F[虚拟机执行 NFA/DFA]
    F --> G[匹配结果]

    style A fill:#e1f5ff
    style G fill:#e8f5e9

专业词汇解释:

  • RE2:Google 开发的正则表达式库( C++ 编写),Go 语言受其启发独立实现了类似算法,并非直接移植
  • 有限状态自动机(Finite Automaton):分为 NFA(非确定有限自动机)和 DFA(确定有限自动机),是正则引擎的核心
  • ReDoS:正则表达式拒绝服务攻击,利用回溯机制使程序卡死

14.3 字符类:预定义快捷方式

字符类是正则的积木块。RE2 提供了一套标准的快捷符号,让你不用一个个字母地敲。

 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"
	"regexp"
)

func main() {
	text := "Hello World! 123. 中文也可以吗?"

	// \d —— 数字 [0-9]
	fmt.Printf("\\d 匹配数字: %v\n", regexp.MustCompile(`\d`).FindAllString(text, -1))
	// \d 匹配数字: [1 2 3]

	// \D —— 非数字
	fmt.Printf("\\D 匹配非数字: %v\n", regexp.MustCompile(`\D`).FindAllString(text, -1))
	// \D 匹配非数字: [H e l l o   W o r l d !   .   中 文 也 可 以 吗 ?]

	// \w —— 单词字符 [a-zA-Z0-9_]
	fmt.Printf("\\w 匹配单词字符: %v\n", regexp.MustCompile(`\w`).FindAllString(text, -1))
	// \w 匹配单词字符: [H e l l o W o r l d 1 2 3]

	// \W —— 非单词字符
	fmt.Printf("\\W 匹配非单词字符: %v\n", regexp.MustCompile(`\W`).FindAllString(text, -1))
	// \W 匹配非单词字符: [   !     .   中 文 也 可 以 吗 ?]

	// \s —— 空白字符(空格、制表符、换行等)
	fmt.Printf("\\s 匹配空白: %v\n", regexp.MustCompile(`\s`).FindAllString(text, -1))
	// \s 匹配空白: [     ]

	// \S —— 非空白字符
	fmt.Printf("\\S 匹配非空白: %v\n", regexp.MustCompile(`\S`).FindAllString(text, -1))
	// \S 匹配非空白: [H e l l o W o r l d ! 1 2 3 . 中 文 也 可 以 吗 ?]

	// . —— 任意字符(除了换行符)
	fmt.Printf(". 匹配任意字符: %v\n", regexp.MustCompile(`..`).FindAllString(text, -1))
	// . 匹配任意字符: [He ll o  W or ld d!  1 23 3.  中文 中可 以也 可?]
}

专业词汇解释:

快捷符等价于含义
\d[0-9]数字
\D[^0-9]非数字
\w[a-zA-Z0-9_]单词字符
\W[^a-zA-Z0-9_]非单词字符
\s[ \t\n\r\f\v]空白字符
\S[^ \t\n\r\f\v]非空白字符
.[^\n]任意字符(默认不匹配换行)

14.4 自定义字符类

预定义字符类虽好,但有时候你需要更精确的选择——比如只匹配元音字母,或者排除某个区间。

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

import (
	"fmt"
	"regexp"
)

func main() {
	// [abc] —— 匹配 a、b、c 中的任意一个
	fmt.Printf("[abc] 匹配: %v\n", regexp.MustCompile(`[aeiou]`).FindAllString("hello world", -1))
	// [abc] 匹配: [e o o]

	// [^abc] —— 匹配除了 a、b、c 之外的所有字符
	fmt.Printf("[^aeiou] 匹配: %v\n", regexp.MustCompile(`[^aeiou\s]`).FindAllString("hello world", -1))
	// [^aeiou] 匹配: [h l l w r l d]

	// [a-z] —— 匹配小写字母区间
	fmt.Printf("[a-z] 匹配小写: %v\n", regexp.MustCompile(`[a-z]`).FindAllString("Hello World 123", -1))
	// [a-z] 匹配小写: [e l l o r l d]

	// [A-Z] —— 匹配大写字母区间
	fmt.Printf("[A-Z] 匹配大写: %v\n", regexp.MustCompile(`[A-Z]`).FindAllString("Hello World 123", -1))
	// [A-Z] 匹配大写: [H W]

	// [0-9] —— 匹配数字,等价于 \d
	fmt.Printf("[0-9] 匹配数字: %v\n", regexp.MustCompile(`[0-9]`).FindAllString("abc123def456", -1))
	// [0-9] 匹配数字: [1 2 3 4 5 6]

	// 组合使用:匹配颜色代码
	colorCode := regexp.MustCompile(`#[0-9A-Fa-f]{6}`)
	codes := colorCode.FindAllString("颜色: #FF5733, #00ff00, #ABC, #12345", -1)
	fmt.Printf("有效的6位颜色码: %v\n", codes)
	// 有效的6位颜色码: [#FF5733 #00ff00]
}

专业词汇解释:

  • 字符类(Character Class):用方括号 [] 包围,表示"匹配其中任意一个字符”
  • 负向字符类(Negated Character Class)[^...] 表示"不匹配其中任意一个字符"
  • 区间(Range)a-z0-9 等,表示 ASCII 码在这个范围内的所有字符

14.5 量词:控制匹配数量

量词决定了前面那个东西要出现几次。* 表示"要多少有多少",+ 表示"至少来一个",? 表示"可有可无"——像极了甲方的需求描述。

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

import (
	"fmt"
	"regexp"
)

func main() {
	text := "aa ab abb abbb abbbb a"

	// * —— 0次或多次
	fmt.Printf("a* 匹配: %v\n", regexp.MustCompile(`ab*`).FindAllString(text, -1))
	// a* 匹配: [a a ab abb abbb abbbb a]

	// + —— 1次或多次
	fmt.Printf("a+ 匹配: %v\n", regexp.MustCompile(`ab+`).FindAllString(text, -1))
	// a+ 匹配: [a ab abb abbb abbbb]

	// ? —— 0次或1次
	fmt.Printf("a?b 匹配: %v\n", regexp.MustCompile(`a?b`).FindAllString(text, -1))
	// a?b 匹配: [ab b b b b]

	// {n} —— 恰好n次
	fmt.Printf("ab{2} 匹配: %v\n", regexp.MustCompile(`ab{2}`).FindAllString(text, -1))
	// ab{2} 匹配: [abb]

	// {n,} —— 至少n次
	fmt.Printf("ab{2,} 匹配: %v\n", regexp.MustCompile(`ab{2,}`).FindAllString(text, -1))
	// ab{2,} 匹配: [abb abbb abbbb]

	// {n,m} —— n到m次
	fmt.Printf("ab{1,3} 匹配: %v\n", regexp.MustCompile(`ab{1,3}`).FindAllString(text, -1))
	// ab{1,3} 匹配: [ab abb abbb]
}

量词对照表:

量词等价于含义
*{0,}0次或多次
+{1,}1次或多次
?{0,1}0次或1次
{n}恰好n次
{n,}至少n次
{n,m}n到m次

14.6 量词的贪婪与非贪婪

这是正则最容易踩坑的地方。默认情况下,*+、量词们都是**贪婪(Greedy)的——它们会尽可能多地匹配字符。但有时候你只想匹配"最少的那一截",这时候就要用非贪婪(Non-Greedy/Lazy)**模式。

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

import (
	"fmt"
	"regexp"
)

func main() {
	html := "<div>Hello</div><div>World</div>"

	// 贪婪匹配 —— 尽可能多地吃字符
	greedy := regexp.MustCompile(`<div>.*</div>`)
	fmt.Printf("贪婪 .*: %v\n", greedy.FindAllString(html, -1))
	// 贪婪 .*: [<div>Hello</div><div>World</div>]
	// 它一口气从第一个 <div> 吃到最后一个 </div>

	// 非贪婪匹配 —— 加上 ? 变成懒汉
	lazy := regexp.MustCompile(`<div>.*?</div>`)
	fmt.Printf("非贪婪 .*?: %v\n", lazy.FindAllString(html, -1))
	// 非贪婪 .*?: [<div>Hello</div> <div>World</div>]
	// 每次只吃到第一个 </div> 就收手

	// 再看一个例子
	text := `"hello" and "world" and "foo"`

	// 贪婪:吃光所有引号内容
	greedyQuote := regexp.MustCompile(`".*"`)
	fmt.Printf("贪婪引号: %v\n", greedyQuote.FindAllString(text, -1))
	// 贪婪引号: ["hello" and "world" and "foo"]

	// 非贪婪:每个引号对单独捕获
	lazyQuote := regexp.MustCompile(`".*?"`)
	fmt.Printf("非贪婪引号: %v\n", lazyQuote.FindAllString(text, -1))
	// 非贪婪引号: ["hello" "world" "foo"]
}

为什么叫贪婪? 想象一个饥饿的怪兽,它看到能吃的东西就往嘴里塞,一直吃到实在塞不下了才停——这就是贪婪匹配。非贪婪就是让怪兽每吃一口就问一下"够了吗",吃够就停。

专业词汇解释:

  • 贪婪匹配(Greedy Match):量词默认行为,尽可能多地匹配字符
  • 非贪婪匹配(Non-Greedy/Lazy Match):在量词后加 ?,尽可能少地匹配字符
  • 回溯(Backtracking):引擎在贪婪失败后会吐回字符重新尝试,非贪婪则相反

14.7 分组:捕获与分组

括号 () 不仅能改变优先级,还能捕获(Capture)匹配的内容。每个括号对就是一个分组,从左到右编号为 $1$2……(在替换时使用)。Go 还支持具名分组(Named Capture Group),给分组起个名字,方便又可读。

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

import (
	"fmt"
	"regexp"
)

func main() {
	text := "2024-03-15"

	// (pattern) —— 捕获分组
	dateRegex := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
	match := dateRegex.FindStringSubmatch(text)
	fmt.Printf("完整匹配: %v\n", match)
	// 完整匹配: [2024-03-15 2024 03 15]
	// match[0] 是整体匹配,match[1] 是第一个分组,...

	// (?:pattern) —— 非捕获分组 —— 只用于优先级,不创建分组
	nonCapture := regexp.MustCompile(`(?:\d{4})-(?:\d{2})-(?:\d{2})`)
	nonMatch := nonCapture.FindStringSubmatch(text)
	fmt.Printf("非捕获分组: %v (只有整体匹配)\n", nonMatch)
	// 非捕获分组: [2024-03-15] (只有整体匹配)

	// 提取时分秒
	timeText := "14:30:45"
	timeRegex := regexp.MustCompile(`(\d{2}):(\d{2}):(\d{2})`)
	timeMatch := timeRegex.FindStringSubmatch(timeText)
	fmt.Printf("时间分解: 时=%s 分=%s 秒=%s\n", timeMatch[1], timeMatch[2], timeMatch[3])
	// 时间分解: 时=14 分=30 秒=45

	// (?P<name>pattern) —— 具名分组(见 14.25 节详述)
	namedRegex := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
	namedMatch := namedRegex.FindStringSubmatch(timeText[:0]) // 这里是空实现,详见14.25
	_ = namedMatch
}

专业词汇解释:

  • 捕获分组(Capturing Group):用 () 包围,匹配内容会被保存到分组中
  • 非捕获分组(Non-Capturing Group):用 (?:...) 包围,参与运算但不创建分组,效率略高
  • 具名分组(Named Capture Group):用 (?P<name>...) 包围,通过名字而非编号访问分组

14.8 锚点:边界定位

锚点不匹配任何具体字符,它们只匹配位置^ 匹配行首,$ 匹配行尾,\b 匹配单词边界——这些在验证格式时特别有用。

 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"
	"regexp"
)

func main() {
	lines := []string{
		"hello world",
		"hello",
		"world",
		"helloworld",
		"say hello world",
	}

	// ^ —— 行首锚点
	for _, line := range lines {
		fmt.Printf("^hello 在 '%s': %v\n", line, regexp.MustCompile(`^hello`).MatchString(line))
	}
	// ^hello 在 'hello world': true
	// ^hello 在 'hello': true
	// ^hello 在 'world': false
	// ^hello 在 'helloworld': true
	// ^hello 在 'say hello world': false

	fmt.Println()

	// $ —— 行尾锚点
	for _, line := range lines {
		fmt.Printf("world$ 在 '%s': %v\n", line, regexp.MustCompile(`world$`).MatchString(line))
	}
	// world$ 在 'hello world': true
	// world$ 在 'hello': false
	// world$ 在 'world': true
	// world$ 在 'helloworld': false
	// world$ 在 'say hello world': false

	fmt.Println()

	// \b —— 单词边界
	text := "cat concat catalog caterpillar"
	fmt.Printf("\\bcat\\b 精确匹配: %v\n", regexp.MustCompile(`\bcat\b`).FindAllString(text, -1))
	// \bcat\b 精确匹配: [cat]
	// "concat" 和 "caterpillar" 里的 "cat" 没有被匹配,因为它们前后是字母

	// \B —— 非单词边界
	fmt.Printf("\\Bcat\\B 内部匹配: %v\n", regexp.MustCompile(`\Bcat\B`).FindAllString(text, -1))
	// \Bcat\B 内部匹配: [cat]
	// "concat" 里的 "cat" 前后都是字母,匹配成功
}

锚点速查表:

锚点含义例子
^行首^Hello 匹配行首的 Hello
$行尾World$ 匹配行尾的 World
\b单词边界\bword\b 精确匹配单词 word
\B非单词边界\Bcat\B 匹配 “concatenate” 中的 cat

14.9 选择:或运算

管道符 | 表示"左边或者右边",就像一个挑剔的食客说"要不然吃火锅,要不然吃烤肉"。

 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"
	"regexp"
)

func main() {
	text := "I like go and python and java"

	// a|b —— 匹配 a 或 b
	re := regexp.MustCompile(`go|python|java`)
	fmt.Printf("匹配语言: %v\n", re.FindAllString(text, -1))
	// 匹配语言: [go python java]

	// 结合分组使用
	phoneText := "电话: 123-456-7890 或 987-654-3210"
	phoneRe := regexp.MustCompile(`(\d{3})-(\d{3})-(\d{4})`)
	phones := phoneRe.FindAllString(phoneText, -1)
	fmt.Printf("电话号码: %v\n", phones)
	// 电话号码: [123-456-7890 987-654-3210]

	// 长选项优先 —— 正则引擎会优先尝试长的分支
	mixed := regexp.MustCompile(`foo|foot|foothold`)
	fmt.Printf("最长优先: %v\n", mixed.FindAllString("foothold", -1))
	// 最长优先: [foot] —— 不是 foo,因为 foot 更长更具体
}

14.10 转义:匹配特殊字符

正则表达式里有些字符有特殊含义——比如 .*+?()[]{}、`——如果你想匹配它们本身,就需要在前面加一个反斜杠 \

 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"
	"regexp"
)

func main() {
	text := "正则里的特殊字符有: . * + ? ( ) [ ] { } \\ 等"

	// \\. —— 匹配字面意义上的点号
	fmt.Printf("字面点号: %v\n", regexp.MustCompile(`\.`).FindAllString(text, -1))
	// 字面点号: [.]

	// \\\\[ —— 匹配反斜杠和方括号
	fmt.Printf("反斜杠: %v\n", regexp.MustCompile(`\\`).FindAllString(text, -1))
	// 反斜杠: [\ \]

	// 匹配数学表达式中的运算符
	math := "3.14 + 2.71 = 5.85"
	ops := regexp.MustCompile(`[\+\-\*/=]`)
	fmt.Printf("数学运算符: %v\n", ops.FindAllString(math, -1))
	// 数学运算符: [ + =]

	// 匹配 IP 地址格式(点号需要转义)
	ip := "服务器 IP: 192.168.1.100 和 10.0.0.1"
	ipRegex := regexp.MustCompile(`\d+\.\d+\.\d+\.\d+`)
	fmt.Printf("IP地址: %v\n", ipRegex.FindAllString(ip, -1))
	// IP地址: [192.168.1.100 10.0.0.1]

	// 匹配括号
	bracket := "函数调用: foo(x, y) 和 bar[a+b]"
	bracketRegex := regexp.MustCompile(`[()\[\]]`)
	fmt.Printf("括号: %v\n", bracketRegex.FindAllString(bracket, -1))
	// 括号: [( ) [ ]]
}

转义字符对照表:

字符含义
\.字面意义上的点号
\*字面意义上的星号
\+字面意义上的加号
\?字面意义上的问号
\( \)字面意义上的圆括号
\[ \]字面意义上的方括号
\{ \}字面意义上的花括号
\\字面意义上的反斜杠

14.11 regexp.Compile:编译正则表达式

regexp.Compile 是最标准的正则编译函数。它接收一个正则字符串,返回一个编译好的 *Regexp 对象和可能发生的错误。如果正则语法有误,返回的 error 就能帮你 debug。

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

import (
	"fmt"
	"regexp"
)

func main() {
	// 正确编译
	re1, err := regexp.Compile(`\d+`)
	if err != nil {
		fmt.Println("编译失败:", err)
	} else {
		fmt.Printf("编译成功: %v\n", re1.FindString("abc123def"))
		// 编译成功: 123
	}

	// 错误编译 —— 看看报错多详细
	_, err = regexp.Compile(`[abc`) // 少了一个 ]
	if err != nil {
		fmt.Println("错误正则编译失败:", err)
		// 错误正则编译失败: error parsing regexp: missing closing ]: `[abc`
	}

	// 编译带分组的有趣正则
	dateRegex, err := regexp.Compile(`(\d{4})/(\d{2})/(\d{2})`)
	if err != nil {
		panic(err)
	}
	text := "今天是 2024/03/15,天气晴朗。"
	match := dateRegex.FindStringSubmatch(text)
	fmt.Printf("找到日期: %s年%s月%s日\n", match[1], match[2], match[3])
	// 找到日期: 2024年03月15日
}

最佳实践: 如果确定正则不会变,用 MustCompile 更简洁;如果正则来自用户输入或配置文件,一定要用 Compile 并处理错误。

14.12 regexp.MustCompile:编译失败就 panic

MustCompile 相当于 Compile 的"强制版"——如果编译失败,直接 panic。这个设计让你在正则写死的情况下省去错误处理代码,但前提是你非常确定你的正则是对的。

 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"
	"regexp"
)

func main() {
	// MustCompile —— 适合正则常量
	// 不推荐用于用户输入或动态正则

	// 编译成功的正则
	re := regexp.MustCompile(`^[a-z]+$`)
	fmt.Println("MustCompile 成功:", re.MatchString("hello"))
	// MustCompile 成功: true

	// 下面这行会 panic!如果你需要处理错误,用 Compile
	// regexp.MustCompile(`[invalid(`) // panic: error parsing regexp

	// 典型场景:全局变量正则,在 init 时编译
	fmt.Println("MustCompile 适合在 init 或全局变量中使用")
}

警告: 永远不要用 MustCompile 处理不可信的正则表达式。一旦用户输入 [invalid(,你的程序直接崩溃,用户体验极差。

14.13 regexp.Match、MatchString、MatchReader:判断是否匹配

三个"判断是否存在"的函数,区别在于输入类型:

  • Match(b []byte) —— 输入字节切片
  • MatchString(s string) —— 输入字符串
  • MatchReader(r io.RuneReader) —— 输入字符流
 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 (
	"bytes"
	"fmt"
	"regexp"
	"strings"
)

func main() {
	re := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)

	// MatchString —— 最常用,输入字符串
	fmt.Println("MatchString:", re.MatchString("2024-03-15")) // true
	fmt.Println("MatchString:", re.MatchString("2024-03-15 ")) // false 末尾有空格

	// Match —— 输入字节切片
	fmt.Println("Match:", re.Match([]byte("2024-03-15"))) // true

	// Match —— 字节切片版
	reader := strings.NewReader("2024-03-15")
	fmt.Println("MatchReader:", re.MatchReader(reader)) // true

	// 验证手机号
	phoneRegex := regexp.MustCompile(`^1[3-9]\d{9}$`)
	testPhones := []string{
		"13812345678", // 合法
		"12345678901", // 非法:首位不对
		"138123456789", // 非法:太长了
		"abc12345678",  // 非法:包含字母
	}
	for _, phone := range testPhones {
		fmt.Printf("手机号 %s 合法: %v\n", phone, phoneRegex.MatchString(phone))
	}
	// 手机号 13812345678 合法: true
	// 手机号 12345678901 合法: false
	// 手机号 138123456789 合法: false
	// 手机号 abc12345678 合法: false

	// 用 bytes.Buffer 构造字节输入
	var buf bytes.Buffer
	buf.WriteString("2024-03-15")
	fmt.Println("Match bytes.Buffer:", re.Match(buf.Bytes())) // true
}

14.14 regexp.Find、FindIndex:返回第一个匹配内容和位置

FindFindIndex 返回第一个匹配的原始字节数据(不是字符串)。区别在于 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
package main

import (
	"fmt"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`\d+`)
	text := "订单号: A123456, 发货日: 20240315"

	// Find —— 返回第一个匹配的字节切片
	match := re.Find(text)
	fmt.Printf("Find 结果: %s\n", match) // Find 结果: 123456
	// 注意:返回的是 []byte,不是 string

	// FindIndex —— 返回匹配的位置 [起始索引, 结束索引)
	loc := re.FindIndex(text)
	fmt.Printf("FindIndex 位置: 起始=%d, 结束=%d\n", loc[0], loc[1])
	fmt.Printf("切片验证: '%s'\n", text[loc[0]:loc[1]])
	// FindIndex 位置: 起始=6, 结束=12
	// 切片验证: '123456'

	// 带分组的情况
	re2 := regexp.MustCompile(`(\d{4})(\d{2})(\d{2})`)
	dateText := "20240315"
	dateMatch := re2.Find(dateText)
	fmt.Printf("日期分组: %s/%s/%s\n", dateMatch[1], dateMatch[2], dateMatch[3])
	// 日期分组: 2024/03/15

	dateLoc := re2.FindIndex(dateText)
	fmt.Printf("日期位置: [%d, %d)\n", dateLoc[0], dateLoc[1])
	// 日期位置: [0, 8)
}

14.15 regexp.FindString、FindStringIndex:字符串版本

FindStringFindStringIndexFind / FindIndex 的字符串友好版本,返回 string 而非 []byte。日常使用中,FindString 系列更常见。

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

import (
	"fmt"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`\w+`)
	text := "Hello, 世界! Go1.21"

	// FindString —— 返回第一个匹配的字符串
	fmt.Printf("FindString: %s\n", re.FindString(text))
	// FindString: Hello

	// FindStringIndex —— 返回字符串位置
	idx := re.FindStringIndex(text)
	fmt.Printf("FindStringIndex: 起始=%d, 结束=%d, 内容='%s'\n", idx[0], idx[1], text[idx[0]:idx[1]])
	// FindStringIndex: 起始=0, 结束=5, 内容='Hello'

	// 找中文
	chinese := regexp.MustCompile(`[\p{Han}]+`)
	hanText := "Hello 世界 Go语言"
	hanIdx := chinese.FindStringIndex(hanText)
	fmt.Printf("中文位置: 起始=%d, 结束=%d, 内容='%s'\n", hanIdx[0], hanIdx[1], hanText[hanIdx[0]:hanIdx[1]])
	// 中文位置: 起始=6, 结束=9, 内容='世界'

	// 找版本号
	version := regexp.MustCompile(`\d+\.\d+\.\d+`)
	v := version.FindString("安装 Go1.21.5 成功")
	fmt.Println("版本号:", v) // 版本号: 1.21.5
}

14.16 regexp.FindSubmatch、FindSubmatchIndex:返回捕获组

带 Submatch 的版本会返回所有分组的匹配结果。[]byte 版本返回 [][]byte,每个元素对应一个分组。

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

import (
	"fmt"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)
	email := "user@example.com"

	// FindSubmatch —— 返回所有分组
	submatches := re.FindSubmatch([]byte(email))
	fmt.Printf("分组数量: %d\n", len(submatches))
	fmt.Printf("整体匹配: %s\n", submatches[0]) // user@example.com
	fmt.Printf("用户名: %s\n", submatches[1])   // user
	fmt.Printf("域名: %s\n", submatches[2])    // example
	fmt.Printf("顶级域: %s\n", submatches[3])  // com

	// FindSubmatchIndex —— 返回各分组的位置
	indices := re.FindSubmatchIndex([]byte(email))
	fmt.Println()
	for i := 0; i < len(indices); i += 2 {
		start, end := indices[i], indices[i+1]
		fmt.Printf("分组 %d 位置: [%d, %d), 内容: '%s'\n", i/2, start, end, email[start:end])
	}
	// 分组 0 位置: [0, 16), 内容: 'user@example.com'
	// 分组 1 位置: [0, 4), 内容: 'user'
	// 分组 2 位置: [5, 12), 内容: 'example'
	// 分组 3 位置: [13, 16), 内容: 'com'
}

14.17 regexp.FindAll、FindAllString:返回所有匹配

一次性找出所有匹配项,-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
package main

import (
	"fmt"
	"regexp"
)

func main() {
	text := "Go1.21 Python3.12 Java17 Rust1.75"

	// FindAll —— 字节切片版
	re := regexp.MustCompile(`[A-Za-z]+`)
	allMatches := re.FindAll([]byte(text), -1)
	fmt.Printf("FindAll [%d个匹配]:\n", len(allMatches))
	for _, m := range allMatches {
		fmt.Printf("  - %s\n", m)
	}
	// FindAll [5个匹配]:
	//   - Go
	//   - Python
	//   - Java
	//   - Rust

	// FindAllString —— 字符串版
	allStrings := re.FindAllString(text, -1)
	fmt.Printf("FindAllString: %v\n", allStrings)
	// FindAllString: [Go Python Java Rust]

	// 提取版本号
	verRe := regexp.MustCompile(`\d+\.\d+`)
	versions := verRe.FindAllString(text, -1)
	fmt.Printf("版本号: %v\n", versions)
	// 版本号: [1.21 3.12 17 1.75]

	// 限制数量
	first2 := verRe.FindAllString(text, 2)
	fmt.Printf("前两个版本: %v\n", first2)
	// 前两个版本: [1.21 3.12]
}

14.18 regexp.FindAllSubmatch、FindAllSubmatchIndex:返回所有捕获组

这是"找出所有匹配 + 每个匹配的所有分组"的组合拳。

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

import (
	"fmt"
	"regexp"
	"strings"
)

func main() {
	// 提取日志中的时间和级别
	logs := []string{
		"[2024-03-15 10:30:45] INFO: 服务器启动",
		"[2024-03-15 10:30:46] WARN: 内存使用率 80%",
		"[2024-03-15 10:30:47] ERROR: 连接失败",
	}

	re := regexp.MustCompile(`\[(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\]\s+(\w+):`)

	// FindAllSubmatch —— 所有匹配的所有分组
	allSubmatches := re.FindAllSubmatch([]byte(strings.Join(logs, "\n")), -1)
	fmt.Println("FindAllSubmatch 结果:")
	for _, submatches := range allSubmatches {
		fmt.Printf("  时间=%s %s, 级别=%s\n", submatches[1], submatches[2], submatches[3])
	}
	// 时间=2024-03-15 10:30:45, 级别=INFO
	// 时间=2024-03-15 10:30:46, 级别=WARN
	// 时间=2024-03-15 10:30:47, 级别=ERROR

	// FindAllSubmatchIndex —— 位置信息
	indices := re.FindAllSubmatchIndex([]byte(strings.Join(logs, "\n")), -1)
	fmt.Println("\n位置信息:")
	for i, loc := range indices {
		fmt.Printf("  第%d条日志: %v\n", i+1, loc)
	}
}

14.19 regexp.ReplaceAll、ReplaceAllLiteral:全部替换

ReplaceAll 是最常用的替换函数,把所有匹配项换成另一个字符串。ReplaceAllLiteral 则不会处理 $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"
	"regexp"
)

func main() {
	text := "user@example.com 管理员: admin@example.com"

	// ReplaceAll —— 标准替换(不处理分组引用)
	re := regexp.MustCompile(`\w+@\w+\.\w+`)
	result := re.ReplaceAll([]byte(text), []byte("[已隐藏邮箱]"))
	fmt.Printf("ReplaceAll: %s\n", result)
	// ReplaceAll: [已隐藏邮箱] 管理员: [已隐藏邮箱]

	// ReplaceAllString —— 字符串版更方便
	result2 := re.ReplaceAllString(text, "***")
	fmt.Printf("ReplaceAllString: %s\n", result2)
	// ReplaceAllString: *** 管理员: ***

	// ReplaceAllLiteral —— 字面替换,不解释 $
	groupRe := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)
	literalResult := groupRe.ReplaceAllLiteral([]byte("user@github.com"), []byte("$1xxx"))
	fmt.Printf("ReplaceAllLiteral $1xxx: %s\n", literalResult)
	// ReplaceAllLiteral $1xxx: $1xxx
	// 因为 ReplaceAllLiteral 把 $1 当普通文本,不会替换成分组内容

	// 数字格式化
	numbers := "1000 2500000 3700000"
	numRe := regexp.MustCompile(`\d+`)
	formatted := numRe.ReplaceAllStringFunc(numbers, func(s string) string {
		return "RMB:" + s
	})
	fmt.Printf("数字格式化: %s\n", formatted)
	// 数字格式化: RMB:1000 RMB:2500000 RMB:3700000
}

14.20 regexp.ReplaceAllFunc:函数式替换

ReplaceAllStringFunc 允许你传入一个函数,每个匹配项都会被这个函数处理后返回替换值。这是实现复杂替换逻辑的利器——比如驼峰转下划线、敏感词分级处理等。

 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"
	"regexp"
	"strings"
)

func main() {
	// 驼峰转蛇形
	camelCase := "helloWorldApiGetUserName"
	wordRe := regexp.MustCompile(`[A-Z]`)
	snake := wordRe.ReplaceAllStringFunc(camelCase, func(match string) string {
		return "_" + strings.ToLower(match)
	})
	// 去掉开头的下划线
	if strings.HasPrefix(snake, "_") {
		snake = snake[1:]
	}
	fmt.Printf("驼峰转蛇形: %s\n", snake)
	// 驼峰转蛇形: hello_world_api_get_user_name

	// 敏感词分级处理
	sensitive := []string{
		"卧槽,这也太牛了",
		"SB才这么干",
		"你个草泥马",
	}
	sensitiveRe := regexp.MustCompile(`卧槽|SB|草泥马`)

	sensitiveMap := map[string]string{
		"卧槽": "哇哦",
		"SB":  "**",
		"草泥马": "***",
	}

	for _, s := range sensitive {
		clean := sensitiveRe.ReplaceAllStringFunc(s, func(match string) string {
			return sensitiveMap[match]
		})
		fmt.Printf("原文: %s -> 脱敏: %s\n", s, clean)
	}
	// 原文: 卧槽,这也太牛了 -> 脱敏: 哇哦,这也太牛了
	// 原文: SB才这么干 -> 脱敏: **才这么干
	// 原文: 你个草泥马 -> 脱敏: 你个***

	// 给所有数字加上千分位
	numbers := "GDP: 1234567890 人均: 98765"
	digitRe := regexp.MustCompile(`\d+`)
	// 注意:Go 标准库没有内置千分位,这里仅演示函数式替换
	thousand := digitRe.ReplaceAllStringFunc(numbers, func(s string) string {
		return "[" + s + "]"
	})
	fmt.Printf("数字标记: %s\n", thousand)
	// 数字标记: GDP: [1234567890] 人均: [98765]
}

14.21 regexp.Expand:基于分组构建替换字符串

ExpandExpandString 让你在替换字符串中引用分组内容。使用 $1${1} 等语法,是 ReplaceAllLiteral 的反面——它专门处理分组引用。

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

import (
	"fmt"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)
	template := []byte("用户 [$1] 域名 [$2] 顶级域 [$3]")

	// 原始邮箱
	email := []byte("alice@google.com")

	// Expand —— 用分组构建新字符串
	result := re.Expand(nil, template, email, nil)
	fmt.Printf("Expand 结果: %s\n", result)
	// Expand 结果: 用户 [alice] 域名 [google] 顶级域 [com]

	// ExpandString —— 字符串版
	template2 := "邮箱: $1@$2.$3"
	result2 := re.ExpandString(nil, template2, "bob@github.com", nil)
	fmt.Printf("ExpandString 结果: %s\n", result2)
	// ExpandString 结果: 邮箱: bob@github.com

	// 交换用户名和域名(经典面试题:互换两个单词)
	swapRe := regexp.MustCompile(`(\w+)@(\w+)`)
	swapTemplate := "$2@$1"
	swapped := swapRe.ExpandString(nil, swapTemplate, "old@new", nil)
	fmt.Printf("交换结果: %s\n", swapped)
	// 交换结果: new@old
}

14.22 regexp.Split:按正则分割

Split 按正则表达式作为分隔符,把字符串切成多个子串。就像 strings.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
package main

import (
	"fmt"
	"regexp"
)

func main() {
	// 按空格和标点分割
	text := "Go,Python;Java:Rust\tSwift\nKotlin"
	re := regexp.MustCompile(`[,\s;:\t\n]+`)
	parts := re.Split(text, -1)
	fmt.Printf("分割结果 [%d个片段]: %v\n", len(parts), parts)
	// 分割结果 [6个片段]: [Go Python Java Rust Swift Kotlin]

	// 提取 IP 和端口
	addr := "服务器1: 192.168.1.100:8080, 服务器2: 10.0.0.1:443"
	addrRe := regexp.MustCompile(`\s*,\s*`)
	servers := addrRe.Split(addr, -1)
	fmt.Printf("服务器列表: %v\n", servers)
	// 服务器列表: [服务器1: 192.168.1.100:8080 服务器2: 10.0.0.1:443]

	// 分割 n 次 —— 限制分割次数
	csv := "a,b,c,d,e"
	csvRe := regexp.MustCompile(`,`)
	// n=2 表示最多分割成 3 段
	limited := csvRe.Split(csv, 3)
	fmt.Printf("限制分割: %v\n", limited)
	// 限制分割: [a b c,d,e]
}

14.23 regexp.LiteralPrefix:获取共享前缀

LiteralPrefix 返回正则表达式匹配的字符串前缀——即所有可能匹配都共享的那部分字面量。这在实现语法高亮、Trie 树等场景下很有用,可以提前判断是否需要进入正则匹配。

 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"
	"regexp"
)

func main() {
	// ^hello 开头的正则,共享前缀是 "hello"
	re1 := regexp.MustCompile(`^hello\s+\w+`)
	literal, complicated := re1.LiteralPrefix()
	fmt.Printf("正则: %s\n", re1.String())
	fmt.Printf("  字面前缀: '%s'\n", literal)
	fmt.Printf("  剩余需要正则匹配: %v\n", complicated == nil)
	// 正则: ^(?i:hello)\s+\w+
	//   字面前缀: 'hello'
	//   剩余需要正则匹配: true

	// 完全字面量的正则
	re2 := regexp.MustCompile(`hello world`)
	literal2, rest2 := re2.LiteralPrefix()
	fmt.Printf("正则: %s\n", re2.String())
	fmt.Printf("  字面前缀: '%s'\n", literal2)
	fmt.Printf("  剩余: %v\n", rest2)
	// 正则: hello world
	//   字面前缀: 'hello world'
	//   剩余: <nil> —— 已经是纯字面量,不需要正则匹配

	// 实际应用:快速判断
	check := func(text string) {
		re := regexp.MustCompile(`^error:\s*\d+`)
		prefix, rest := re.LiteralPrefix()
		if rest == nil {
			// 纯字面量
			fmt.Printf("  '%s' 精确匹配 '%s': %v\n", text, prefix, text == prefix)
		} else {
			// 需要正则
			fmt.Printf("  '%s' 用前缀 '%s' + 正则\n", text, prefix)
		}
	}
	check("error: 404")
	check("error: 500")
}

14.24 regexp.Longest:让引擎选择最长匹配

默认情况下,Go 的正则引擎在存在多种可能时选择最先匹配的分支。但如果你希望它选择最长匹配(类似 Perl 的默认行为),可以调用 Longest() 方法。

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

import (
	"fmt"
	"regexp"
)

func main() {
	// 测试两种匹配模式
	text := "ab"

	// 默认模式(Leftmost-First)
	re1 := regexp.MustCompile(`a|ab`)
	fmt.Printf("默认 leftmost-first: %v\n", re1.FindString(text))
	// 默认 leftmost-first: a —— 先尝试 a,匹配成功就返回

	// Longest 模式
	re2 := regexp.MustCompile(`a|ab`)
	re2.Longest() // 切换为最长匹配模式
	fmt.Printf("Longest 模式: %v\n", re2.FindString(text))
	// Longest 模式: ab —— 虽然先尝试 a,但最终选择最长的 ab

	// 另一个例子
	text2 := "aaaa"
	re3 := regexp.MustCompile(`a+|a`)
	re4 := regexp.MustCompile(`a+|a`)
	re4.Longest()

	fmt.Printf("默认: %v\n", re3.FindString(text2)) // a —— leftmost
	fmt.Printf("Longest: %v\n", re4.FindString(text2)) // aaaa —— longest
}

注意: Longest() 必须在使用正则之前调用,因为它会改变正则引擎的行为模式。

14.25 Named Capture Groups:具名分组

具名分组让你用名字而非数字访问分组。这在正则复杂、层级多的时候特别有用——match[1]match["username"] 哪个更可读?

Go 中有两种方式使用具名分组:

 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"
	"regexp"
)

func main() {
	// Go 的具名分组语法: (?P<name>pattern)
	re := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)

	text := "2024-03-15"
	match := re.FindStringSubmatch(text)
	names := re.SubexpNames()

	fmt.Printf("原始匹配: %v\n", match)
	// 原始匹配: [2024-03-15 2024 03 15]

	// 按名字访问分组
	for i, name := range names {
		if i > 0 && name != "" {
			fmt.Printf("  分组 %d [%s]: %s\n", i, name, match[i])
		}
	}
	// 分组 1 [year]: 2024
	// 分组 2 [month]: 03
	// 分组 3 [day]: 15

	// 使用 submatches map —— Go 1.15+
	sub := re.SubexpMap()
	fmt.Printf("\n分组 Map:\n")
	fmt.Printf("  year: %s\n", sub["year"])
	fmt.Printf("  month: %s\n", sub["month"])
	fmt.Printf("  day: %s\n", sub["day"])
	// year: 2024
	// month: 03
	// day: 15

	// 具名分组在替换中的应用
	template := "日期: ${year}年${month}月${day}日"
	result := re.ExpandString(nil, template, text, nil)
	fmt.Printf("\n替换结果: %s\n", result)
	// 替换结果: 日期: 2024年03月15日
}

14.26 regexp 的性能特点:编译后可以复用

这是使用 regexp 最最重要的性能原则:正则表达式编译成本很高,必须复用编译结果。

 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"
	"regexp"
	"time"
)

func main() {
	// 反面教材:在循环里重复编译
	badText := "email: user@ex.com phone: 13812345678"
	iterations := 10000

	// 错误方式:每次循环都编译
	badStart := time.Now()
	for i := 0; i < iterations; i++ {
		re := regexp.MustCompile(`\w+@\w+\.\w+`) // 每次都重新编译!
		re.FindString(badText)
	}
	badDuration := time.Since(badStart)
	fmt.Printf("循环内重复编译: %v\n", badDuration)
	// 循环内重复编译: ~1.5s(非常慢)

	// 正确方式:循环外编译一次
	re := regexp.MustCompile(`\w+@\w+\.\w+`) // 编译一次
	goodStart := time.Now()
	for i := 0; i < iterations; i++ {
		re.FindString(badText) // 复用
	}
	goodDuration := time.Since(goodStart)
	fmt.Printf("循环外单次编译: %v\n", goodDuration)
	// 循环外单次编译: ~5ms(快300倍)

	// 对比
	fmt.Printf("\n性能提升: %d 倍\n", badDuration.Milliseconds()/goodDuration.Milliseconds())
}

性能原则总结:

flowchart TD
    A[输入文本] --> B{是否已编译?}
    B -->|否| C[调用 Compile/MustCompile]
    C --> D[编译耗时 0.1-10ms]
    D --> E[保存到变量]
    E --> F[执行 Match/Find/Replace]
    B -->|是| F
    F --> G[匹配结果]

    style C fill:#ffcccc
    style E fill:#ccffcc

记住: 编译正则是一次性的,匹配是无数次次的。把编译放在循环外,就像把模具做好反复用,而不是每次都重新开模。

14.27 regexp/syntax:正则表达式语法解析器

Go 的 regexp/syntax 包提供了正则表达式的语法解析器。你可以用它来分析、验证、甚至重写正则表达式,而不需要实际执行匹配。这在开发正则调试器、语法高亮、静态分析等工具时非常有用。

 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"
	"regexp/syntax"
)

func main() {
	// 解析正则表达式字符串为 AST
	parsed, err := syntax.Parse(`hello\s+(world|golang)\d*`, syntax.Perl)
	if err != nil {
		fmt.Println("解析失败:", err)
		return
	}

	fmt.Printf("解析后的 AST:\n")
	fmt.Printf("  类型: %T\n", parsed)
	fmt.Printf("  字符串表示: %s\n", parsed.String())

	// 遍历 AST 节点
	fmt.Println("\nAST 结构:")
	walk(parsed, 0)

	// 简化正则
	simplified := parsed.Simplify()
	fmt.Printf("\n简化后: %s\n", simplified.String())
}

func walk(expr syntax.Expr, indent int) {
	prefix := "  "
	for i := 0; i < indent; i++ {
		prefix += "  "
	}

	switch e := expr.(type) {
	case *syntax.Regexp:
		fmt.Printf("%s类型: %s, 操作符: %v\n", prefix, e.Op, e.Name)
		for _, sub := range e.Sub {
			walk(sub, indent+1)
		}
	default:
		fmt.Printf("%s值: %v\n", prefix, e)
	}
}

regexp/syntax 常用函数:

函数作用
syntax.Parse(str, mode)将正则字符串解析为 AST
(*Regexp).Simplify()简化 AST(如去除冗余)
(*Regexp).String()将 AST 转换回字符串
syntax.IsValid(str)判断正则是否合法

14.28 正则 vs 字符串操作:何时不该用正则

正则虽强,但不是万能的。有些场景用字符串操作更简单、更高效、更安全。

 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"
	"regexp"
	"strings"
)

func main() {
	// 场景1:检查字符串是否包含子串 —— 用 strings.Contains
	text := "Hello, World!"
	if strings.Contains(text, "World") {
		fmt.Println("找到 World")
	}
	// 正则?杀鸡用牛刀
	// regexp.MustCompile(`World`).MatchString(text) // 完全可以,但过度设计

	// 场景2:检查前缀/后缀 —— 用 strings.HasPrefix/HasSuffix
	url := "https://golang.org"
	if strings.HasPrefix(url, "https://") {
		fmt.Println("是 HTTPS 链接")
	}
	if strings.HasSuffix(url, ".org") {
		fmt.Println("是 .org 域名")
	}

	// 场景3:字符串替换 —— 用 strings.Replace
	dirty := "hello world world world"
	clean := strings.ReplaceAll(dirty, "world", "go")
	fmt.Println("ReplaceAll:", clean)
	// vs regexp.MustCompile(`world`).ReplaceAllString(dirty, "go")

	// 场景4:分割字符串 —— 用 strings.Split
	csv := "a,b,c,d"
	parts := strings.Split(csv, ",")
	fmt.Println("Split:", parts)

	// 场景5:大小写转换 —— 用 strings.ToLower/ToUpper
	s := "Hello WORLD"
	fmt.Printf("ToLower: %s\n", strings.ToLower(s))
	fmt.Printf("ToUpper: %s\n", strings.ToUpper(s))

	// 场景6:去除首尾空白 —— 用 strings.TrimSpace
	padded := "  \t hello \n"
	trimmed := strings.TrimSpace(padded)
	fmt.Printf("TrimSpace: '%s'\n", trimmed)

	// 场景7:检查是否全是数字/字母
	digits := "12345"
	if strings.Trim(digits, "0123456789") == "" {
		fmt.Println("全是数字")
	}
	// vs regexp.MustCompile(`^\d+$`).MatchString(digits)
}

什么时候用正则,什么时候用字符串操作?

场景推荐方式原因
包含检查strings.ContainsO(n),无需额外开销
前缀/后缀strings.HasPrefix/HasSuffixO(k),k 为前缀长度
简单替换strings.ReplaceAll无需正则引擎
固定分隔符分割strings.Split简单高效
格式验证(邮箱、IP等)regexp复杂规则,正则更简洁
提取结构化数据regexp分组提取很方便
敏感词替换regexp需要动态词库

本章小结

本章我们深入探索了 Go 标准库中的 regexp 包,它是建立在 Google RE2 引擎之上的正则表达式实现。以下是本章的核心要点:

核心概念

  • RE2 引擎:Google 开发的确定性有限自动机引擎,性能优秀且绝对安全(不支持回溯 ReDoS 攻击),但不支持反向引用和 lookahead/lookbehind
  • 字符类\d \D \w \W \s \S 和自定义 [...] 是正则的原子单元
  • 量词* + ? {n} {n,} {n,m} 控制匹配次数,默认贪婪,加 ? 变非贪婪
  • 分组:捕获分组 (pattern)、非捕获分组 (?:pattern)、具名分组 (?P<name>pattern)
  • 锚点^ $ \b \B 匹配位置而非字符

核心 API

函数作用
regexp.Compile编译正则,返回 *Regexp 和错误
regexp.MustCompile编译正则,失败 panic
Match/MatchString判断是否匹配
Find/FindString找第一个匹配
FindAll/FindAllString找所有匹配
FindSubmatch找第一个匹配及其分组
FindAllSubmatch找所有匹配及其分组
ReplaceAll全部替换
ReplaceAllFunc函数式替换
Split按正则分割
Expand基于分组构建字符串
LiteralPrefix获取字面前缀
Longest切换为最长匹配模式

性能黄金法则

正则只编译一次,在循环外复用。 编译成本在毫秒级,但匹配成本在微秒级。循环内重复编译是 regexp 性能的第一杀手。

何时不用正则

对于简单的字符串操作(Contains、HasPrefix、Split、Replace 等),标准库的 strings 包更简单、更高效。正则是为复杂模式设计的工具,不是字符串操作的替代品。


“有些问题你以为是正则问题,其实它是字符串问题。” —— 《Go 语言圣经》附录

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