正则很好用,但不要让它变成谜语
正则表达式适合处理文本模式:校验格式、提取字段、替换内容、扫描日志。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 清楚表达的,就不要上正则;正则太长时,拆成小函数或加注释解释意图;业务校验不要把所有复杂性都压进一个表达式里。
写得好的正则能让文本处理很利落,写得糟的正则会让代码没人敢碰。入门阶段先追求可读和足够准确,再考虑更复杂的模式。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。