正则表达式:文本处理的瑞士军刀

全面掌握 Go 语言的正则表达式:语法、匹配、捕获组、性能优化和实战案例

正则表达式:文本处理的瑞士军刀

如果你要在代码里验证邮箱格式、提取日志中的时间戳、替换文本中的敏感信息、或者解析复杂的配置文件——正则表达式(Regular Expression,简称 regex)就是你的瑞士军刀。

正则表达式是一种强大的文本模式匹配工具,几乎每一种主流编程语言都支持它。Go 语言的 regexp 包实现了 RE2 语法,这是一种安全、高效、但略有简化的正则表达式变体。

今天我们就来全面学习 Go 的正则表达式,从基础语法到高级技巧,让你成为文本处理大师。

什么是正则表达式?

正则表达式是一个描述文本模式的字符串。比如:

  • \d{3}-\d{4} 匹配类似 “123-4567” 的电话号码
  • [a-z]+@[a-z]+\.[a-z]+ 匹配类似 “user@example.com” 的邮箱
  • ^\d{4}-\d{2}-\d{2}$ 匹配类似 “2021-02-27” 的日期

正则表达式看起来像天书,但一旦你掌握了它的语法规则,就会发现它其实很有逻辑。

第一个正则表达式

package main

import (
	"fmt"
	"regexp"
)

func main() {
	// 编译正则表达式
	re := regexp.MustCompile(`\d+`)

	// 检查是否匹配
	fmt.Println(re.MatchString("Hello 123 World"))   // true
	fmt.Println(re.MatchString("Hello World"))        // false

	// 查找第一个匹配
	fmt.Println(re.FindString("Hello 123 World 456")) // "123"

	// 查找所有匹配
	fmt.Println(re.FindAllString("Hello 123 World 456", -1))
	// [123 456]
}

⚠️ 注意:Go 的正则表达式字符串用反引号 ` 包裹,这样就不需要转义反斜杠。如果用双引号,你需要写 \\d+

编译 vs 不编译

Go 提供了两种方式执行正则表达式:

1. 预编译(推荐)

re := regexp.MustCompile(`pattern`)  // 失败时 panic
// 或者
re, err := regexp.Compile(`pattern`)  // 返回错误

预编译的正则表达式可以重复使用,性能更好。

2. 一次性使用

matched, _ := regexp.MatchString(`pattern`, "text")

适合只使用一次的情况。

💡 最佳实践:如果是频繁使用的正则表达式,用全局变量预编译:

var emailRE = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)

func IsValidEmail(email string) bool {
	return emailRE.MatchString(email)
}

正则表达式语法速查

基本匹配

语法说明例子
abc匹配字面字符串“abc”
.匹配任意字符(除了换行)“a.c” 匹配 “abc”、“axc”
\d匹配数字 [0-9]\d{3} 匹配 “123”
\D匹配非数字\D+ 匹配 “abc”
\w匹配单词字符 [a-zA-Z0-9_]\w+ 匹配 “user_1”
\W匹配非单词字符\W+ 匹配 “@#$”
\s匹配空白字符\s+ 匹配 " \t\n"
\S匹配非空白字符\S+ 匹配 “text”

量词

语法说明例子
*0 次或多次a* 匹配 “"、“a”、“aaa”
+1 次或多次a+ 匹配 “a”、“aaa”
?0 次或 1 次a? 匹配 “"、“a”
{n}精确 n 次\d{4} 匹配 “2021”
{n,}至少 n 次\d{2,} 匹配 “12”、“1234”
{n,m}n 到 m 次\d{2,4} 匹配 “12”、“123”、“1234”

字符类

语法说明例子
[abc]匹配 a、b 或 c[aeiou] 匹配元音
[^abc]匹配不是 a、b、c 的字符[^0-9] 匹配非数字
[a-z]匹配 a 到 z 之间的字符[A-Za-z] 匹配字母

边界

语法说明例子
^字符串开头^Hello 匹配 “Hello world”
$字符串结尾world$ 匹配 “Hello world”
\b单词边界\bword\b 匹配独立的 “word”

分组和引用

语法说明例子
(abc)捕获组(\d+) 捕获数字
(?:abc)非捕获组(?:abc)+ 不捕获
\1引用第一个捕获组(\w+)\s+\1 匹配 “hello hello”
(?P<name>abc)命名捕获组(?P<year>\d{4})

选择

语法说明例子
abc|def匹配 abc 或 defcat|dog

常用操作

Match:检查是否匹配

re := regexp.MustCompile(`^\d+$`)

fmt.Println(re.MatchString("12345"))  // true
fmt.Println(re.MatchString("123a5"))  // false

// 也可以匹配字节切片
fmt.Println(re.Match([]byte("12345")))  // true

Find:查找匹配

re := regexp.MustCompile(`\d+`)

// 查找第一个
fmt.Println(re.FindString("abc 123 def 456"))  // "123"

// 查找所有(-1 表示不限数量)
fmt.Println(re.FindAllString("abc 123 def 456", -1))
// [123 456]

// 限制数量
fmt.Println(re.FindAllString("abc 123 def 456", 1))
// [123]

// 查找匹配的索引
fmt.Println(re.FindStringIndex("abc 123"))
// [4 7](匹配的起止索引)

Replace:替换匹配

re := regexp.MustCompile(`\d+`)

// 替换为固定字符串
fmt.Println(re.ReplaceAllString("abc 123 def 456", "X"))
// "abc X def X"

// 替换为函数
result := re.ReplaceAllStringFunc("abc 123 def 456", func(match string) string {
	n, _ := strconv.Atoi(match)
	return strconv.Itoa(n * 2)
})
fmt.Println(result)  // "abc 246 def 912"

Split:按模式分割

re := regexp.MustCompile(`[\s,]+`)

fmt.Println(re.Split("a,b, c  d", -1))
// [a b c d]

捕获组

捕获组让你能提取匹配中的特定部分。

基本捕获组

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

// FindStringSubmatch 返回整个匹配和各个捕获组
match := re.FindStringSubmatch("今天的日期是 2021-02-27。")
fmt.Println(match)
// [2021-02-27 2021 02 27]

// match[0] 是整个匹配
// match[1] 是第一个捕获组(年)
// match[2] 是第二个捕获组(月)
// match[3] 是第三个捕获组(日)

year, month, day := match[1], match[2], match[3]
fmt.Printf("年: %s, 月: %s, 日: %s\n", year, month, day)

命名捕获组

命名捕获组让代码更易读:

re := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)

match := re.FindStringSubmatch("2021-02-27")

// 获取捕获组的名字和索引
names := re.SubexpNames()
result := make(map[string]string)

for i, name := range names {
	if i != 0 && name != "" {
		result[name] = match[i]
	}
}

fmt.Println(result)
// map[day:27 month:02 year:2021]

fmt.Println("年份:", result["year"])

查找所有捕获组

re := regexp.MustCompile(`(\w+)=(\d+)`)

text := "a=1 b=2 c=3"
matches := re.FindAllStringSubmatch(text, -1)

for _, match := range matches {
	fmt.Printf("%s = %s\n", match[1], match[2])
}
// a = 1
// b = 2
// c = 3

性能优化

1. 预编译正则表达式

// ❌ 不好:每次调用都重新编译
func IsValidEmail(email string) bool {
	re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
	return re.MatchString(email)
}

// ✅ 好:只编译一次
var emailRE = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)

func IsValidEmail(email string) bool {
	return emailRE.MatchString(email)
}

2. 用 Find 而不是 Match

如果你需要知道匹配的内容或位置,用 Find 而不是先 MatchFind

// ❌ 不好:匹配两次
if re.MatchString(s) {
    result := re.FindString(s)
}

// ✅ 好:只匹配一次
result := re.FindString(s)
if result != "" {
    // 找到了
}

3. 避免过度的正则

不是所有文本处理都需要正则表达式:

// ❌ 用正则检查前缀
re := regexp.MustCompile(`^Hello`)
re.MatchString(s)

// ✅ 直接用 strings.HasPrefix
strings.HasPrefix(s, "Hello")

strings 包比正则快得多,能用 strings 就不要用 regexp

实战:文本处理工具集

让我们用正则表达式实现一些实用的文本处理功能:

1. 邮箱验证

var emailRE = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)

func IsValidEmail(email string) bool {
	return emailRE.MatchString(email)
}

// 测试
fmt.Println(IsValidEmail("user@example.com"))     // true
fmt.Println(IsValidEmail("invalid@.com"))         // false
fmt.Println(IsValidEmail("@example.com"))         // false

2. 提取 URL

var urlRE = regexp.MustCompile(`https?://[^\s"'>]+`)

func ExtractURLs(text string) []string {
	return urlRE.FindAllString(text, -1)
}

text := `访问我们的网站 https://example.com 或博客 http://blog.example.com`
fmt.Println(ExtractURLs(text))
// [https://example.com http://blog.example.com]

3. 解析日志

var logRE = regexp.MustCompile(`\[(?P<level>\w+)\]\s+(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(?P<message>.+)`)

type LogEntry struct {
	Level   string
	Time    string
	Message string
}

func ParseLog(line string) (*LogEntry, error) {
	match := logRE.FindStringSubmatch(line)
	if match == nil {
		return nil, fmt.Errorf("invalid log format")
	}

	result := make(map[string]string)
	for i, name := range logRE.SubexpNames() {
		if i != 0 && name != "" {
			result[name] = match[i]
		}
	}

	return &LogEntry{
		Level:   result["level"],
		Time:    result["time"],
		Message: result["message"],
	}, nil
}

// 测试
entry, _ := ParseLog("[INFO] 2021-02-27 16:05:00 Server started")
fmt.Printf("%+v\n", entry)
// &{Level:INFO Time:2021-02-27 16:05:00 Message:Server started}

4. 敏感信息脱敏

var (
	phoneRE = regexp.MustCompile(`(\d{3})\d{4}(\d{4})`)
	emailRE = regexp.MustCompile(`([a-zA-Z0-9._%+-]{2})[a-zA-Z0-9._%+-]*(@[a-zA-Z0-9.-]+)`)
	idRE    = regexp.MustCompile(`(\d{6})\d{8}(\d{4})`)
)

func MaskSensitiveInfo(text string) string {
	// 手机号:13812345678 → 138****5678
	text = phoneRE.ReplaceAllString(text, "${1}****${2}")
	
	// 邮箱:zhangsan@example.com → zh***@example.com
	text = emailRE.ReplaceAllString(text, "${1}***${2}")
	
	// 身份证:110101199001011234 → 110101********1234
	text = idRE.ReplaceAllString(text, "${1}********${2}")
	
	return text
}

text := "联系人:张三,手机:13812345678,邮箱:zhangsan@example.com,身份证:110101199001011234"
fmt.Println(MaskSensitiveInfo(text))
// 联系人:张三,手机:138****5678,邮箱:zh***@example.com,身份证:110101********1234

5. 模板变量替换

var templateVarRE = regexp.MustCompile(`\{\{\s*(\w+)\s*\}\}`)

func RenderTemplate(template string, vars map[string]string) string {
	return templateVarRE.ReplaceAllStringFunc(template, func(match string) string {
		// 提取变量名
		submatch := templateVarRE.FindStringSubmatch(match)
		name := submatch[1]
		
		if value, ok := vars[name]; ok {
			return value
		}
		return match  // 未找到则保留原样
	})
}

template := "你好,{{ name }}!你的订单 {{ order_id }} 已经发货。"
vars := map[string]string{
	"name":     "张三",
	"order_id": "ORD-12345",
}

fmt.Println(RenderTemplate(template, vars))
// 你好,张三!你的订单 ORD-12345 已经发货。

RE2 vs PCRE

Go 使用的是 RE2 正则引擎,和传统的 PCRE(Perl Compatible Regular Expressions)有一些区别:

RE2 不支持的特性

  1. 反向引用\1\2
  2. 环视(Lookaround)(?=...)(?!...)(?<=...)(?<!...)
  3. 条件语句(?(...)...)
  4. 回溯控制(*PRUNE)(*SKIP)

为什么选择 RE2?

RE2 的设计目标是保证线性时间复杂度——不管输入多长、模式多复杂,匹配时间都是 O(n)。而 PCRE 在某些情况下会陷入指数级的回溯。

这意味着在 Go 中使用正则表达式不会有 ReDoS(正则表达式拒绝服务)风险,这是生产环境非常重要的安全保证。

常见陷阱

1. 贪婪 vs 非贪婪

默认量词是贪婪的,会匹配尽可能多的内容:

re := regexp.MustCompile(`<.*>`)
fmt.Println(re.FindString("<a> <b> <c>"))
// <a> <b> <c>(贪婪:匹配从第一个 < 到最后一个 >)

// 用 ? 让量词变成非贪婪
re = regexp.MustCompile(`<.*?>`)
fmt.Println(re.FindAllString("<a> <b> <c>", -1))
// [<a> <b> <c>](非贪婪:每个标签独立匹配)

2. . 不匹配换行符

re := regexp.MustCompile(`a.b`)
fmt.Println(re.MatchString("a\nb"))  // false

// 用 (?s) 让 . 匹配换行符
re = regexp.MustCompile(`(?s)a.b`)
fmt.Println(re.MatchString("a\nb"))  // true

3. 字符集范围

// ❌ 不是你想的那样
re := regexp.MustCompile(`[A-z]+`)  // 包含了 [] ^ _ ` 等字符!

// ✅ 正确写法
re = regexp.MustCompile(`[A-Za-z]+`)

小结

今天我们全面学习了 Go 的正则表达式:

  1. 基础语法:字面字符、字符类、量词、边界
  2. 编译方式MustCompileCompileMatchString
  3. 常用操作MatchFindReplaceSplit
  4. 捕获组:基本捕获组、命名捕获组
  5. 性能优化:预编译、避免过度使用正则
  6. RE2 特性:线性时间、不支持反向引用和环视
  7. 常见陷阱:贪婪/非贪婪、. 不匹配换行、字符集范围

正则表达式是一个强大的工具,但也要谨慎使用。简单的问题用 strings 包就够了,复杂的模式匹配才用正则。

练习时间

  1. 手机号验证:写一个函数,验证中国大陆手机号(11 位,1 开头)
  2. URL 解析:提取 URL 中的协议、域名、路径、查询参数
  3. Markdown 处理:把 Markdown 的链接 [text](url) 转换成 HTML
  4. CSV 解析:用正则表达式解析 CSV 格式的数据(注意引号转义)
  5. IP 地址验证:验证 IPv4 地址是否合法(注意每个部分在 0-255 之间)

系列总结

恭喜你!到这里你已经完成了 Go 语言进阶入门系列的全部 10 篇文章!🎉

让我们回顾一下这一系列的内容:

  1. Goroutine:Go 的轻量级并发单元
  2. Channel:goroutine 之间的通信管道
  3. sync 包:传统的同步工具(锁、等待组等)
  4. Context:超时、取消和值传递
  5. 文件 I/O:读写文件和处理目录
  6. JSON 处理:序列化和反序列化
  7. HTTP 编程:构建 Web 客户端和服务器
  8. 测试:单元测试、基准测试、测试覆盖率
  9. Go Modules:现代的依赖管理
  10. 正则表达式:强大的文本模式匹配

这些知识涵盖了 Go 语言最实用的部分。掌握了这些,你已经能构建真实的 Go 应用了。

Go 语言的魅力在于它的简洁——用最少的语言特性解决最多的问题。希望这个系列能帮助你更好地理解和使用 Go 语言。

继续学习,继续实践,Go 的未来属于你!🚀


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页