Go 正则表达式入门:用 regexp 做校验、提取和替换

本文通过邮箱校验、日志提取和文本替换示例讲解 Go regexp 包的常见用法、编译时机和可维护性边界。

正则很好用,但不要让它变成谜语

正则表达式适合处理文本模式:校验格式、提取字段、替换内容、扫描日志。Go 标准库提供了 regexp 包,使用起来比较直接。你可以编译一个正则,判断字符串是否匹配,也可以查找子匹配,把日志行拆成结构化数据。

但正则也容易被滥用。一个过长的正则会让代码像密文,后面没人敢改。入门阶段要学的不只是 API,还包括边界:什么时候用正则,什么时候用 strings 包更清楚,正则应该在哪里编译,错误怎么处理。

这篇文章用几个常见场景讲 Go 正则:邮箱粗校验、日志字段提取、手机号脱敏和多空格规范化。

最小匹配

使用:

matched, err := regexp.MatchString(`^go`, "golang")
if err != nil {
	return err
}
fmt.Println(matched)

regexp.MatchString 每次都会编译表达式。偶尔用一次没问题,频繁调用时应该先编译:

var goPrefix = regexp.MustCompile(`^go`)

func hasGoPrefix(s string) bool {
	return goPrefix.MatchString(s)
}

MustCompile 编译失败会 panic,适合包级固定正则。因为正则是你写死在代码里的,如果它错了,程序启动失败比运行一半才报错更好。

如果正则来自用户输入,不能用 MustCompile,要返回错误:

re, err := regexp.Compile(pattern)
if err != nil {
	return fmt.Errorf("invalid pattern: %w", err)
}

邮箱校验要克制

很多人会找一个特别长的邮箱正则。真实世界的邮箱格式比想象复杂,完全符合标准的正则很难读。业务里通常只需要粗略校验,再依靠邮件验证流程确认。

var emailPattern = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)

func validEmail(email string) bool {
	email = strings.TrimSpace(email)
	return emailPattern.MatchString(email)
}

这个正则表达的是:不能有空白,必须有一个 @,后面有点号。它不完美,但对很多注册表单的第一层校验已经够用。

如果业务要求非常严格,最好使用成熟库或发送验证邮件,而不是在正则里堆越来越多规则。正则应该让规则更清楚,而不是把复杂性藏起来。

提取日志字段

日志行:

2020-04-17 19:10:00 GET /api/users 200

正则:

var logPattern = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(\w+)\s+(\S+)\s+(\d{3})$`)

解析:

type AccessLog struct {
	Time   string
	Method string
	Path   string
	Status string
}

func parseLog(line string) (AccessLog, bool) {
	matches := logPattern.FindStringSubmatch(line)
	if matches == nil {
		return AccessLog{}, false
	}

	return AccessLog{
		Time:   matches[1],
		Method: matches[2],
		Path:   matches[3],
		Status: matches[4],
	}, true
}

FindStringSubmatch 返回完整匹配和每个括号捕获的内容。matches[0] 是整行,后面才是分组。使用前要检查 nil,否则不匹配时访问下标会 panic。

调用:

logLine := "2020-04-17 19:10:00 GET /api/users 200"
entry, ok := parseLog(logLine)
if !ok {
	fmt.Println("invalid log")
	return
}
fmt.Printf("%+v\n", entry)

替换和脱敏

手机号脱敏:

var phonePattern = regexp.MustCompile(`(\d{3})\d{4}(\d{4})`)

func maskPhone(s string) string {
	return phonePattern.ReplaceAllString(s, `${1}****${2}`)
}

使用:

fmt.Println(maskPhone("电话 13812345678"))

输出:

电话 138****5678

规范化空白:

var spaces = regexp.MustCompile(`\s+`)

func normalizeSpace(s string) string {
	return strings.TrimSpace(spaces.ReplaceAllString(s, " "))
}

如果只是判断前缀、后缀、包含、简单切分,优先用 strings 包。比如:

strings.HasPrefix(path, "/api/")
strings.Contains(title, "Go")
strings.Split(input, ",")

这些比正则更清楚,也通常更快。

小结

Go 的 regexp 包适合校验、提取和替换文本模式。固定正则可以用 regexp.MustCompile 放在包级变量,用户输入的正则要用 regexp.Compile 返回错误。提取分组时使用 FindStringSubmatch,并检查是否匹配。

正则要克制。能用 strings 清楚表达的,就不要上正则;正则太长时,拆成小函数或加注释解释意图;业务校验不要把所有复杂性都压进一个表达式里。

写得好的正则能让文本处理很利落,写得糟的正则会让代码没人敢碰。入门阶段先追求可读和足够准确,再考虑更复杂的模式。

继续阅读

探索更多技术文章

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

全部文章 返回首页