编译一次,然后匹配
把模式写成反引号括起来的原始字符串字面量。在普通的双引号字符串中,每个反斜杠都要写两次("\\d+"),很快就会变得难以阅读。
如果模式无效,regexp.MustCompile 会 panic,对于写在源代码中的模式,这正是你想要的:拼写错误在启动时就立即失败,而不是等到第一个请求时。对于来自用户或配置的模式,使用 regexp.Compile 并处理错误:
re, err := regexp.Compile(userPattern)
if err != nil {
return fmt.Errorf("bad pattern: %w", err)
}
只编译一次。 编译比匹配昂贵得多。在每个请求或每一行都会执行的函数中调用 regexp.MustCompile,每次都会重复这项工作。把编译好的 *regexp.Regexp 放在包级 var 或结构体字段中。编译好的 regexp 可以安全地被多个 goroutine 同时使用。
regexp.MatchString(pattern, s) 在一次调用中完成编译和匹配。用于一次性检查没问题,放在循环里就是浪费。
查找匹配
方法名遵循一个规律:Find + All? + String? + Submatch? + Index?。
| 部分 | 含义 |
|---|---|
All | 所有不重叠的匹配;接收一个上限 n(-1 表示全部) |
String | 作用于 string;没有它时,方法接收并返回 []byte |
Submatch | 同时返回捕获组 |
Index | 返回字节偏移量,而不是文本 |
没有匹配时 FindString 返回 "",这和匹配到一个空字符串无法区分。当模式可能匹配空文本时,使用 FindStringIndex(没有匹配时返回 nil),或者先调用 MatchString。没有任何匹配时,All 系列方法返回 nil。
捕获组
圆括号表示捕获。FindStringSubmatch 在索引 0 返回整个匹配,然后每个组各占一项:
对结果按索引取值之前,一定要先检查是否为 nil;没有匹配时,m[1] 会引发 panic。
(?:...) 是不捕获的分组,用于选择或重复:(?:ab)+。命名组写作 (?P<name>...),从 Go 1.22 起也可以写作 (?<name>...)。
替换
组引用有一个经典陷阱:"$1x" 会被解读为名为 1x 的组,而这个组不存在,于是展开为空字符串。应该写成 "${1}x"。
对于固定的字符串,strings.ReplaceAll、strings.Contains 和 strings.Split 比正则更简单也更快。当你要找的文本是某种形态而不是一个固定值时,再使用 regexp。
语法速查
Go 使用 RE2 语法,它涵盖了大家熟悉的 Perl 风格的功能:
| 语法 | 匹配 |
|---|---|
. | 除换行符外的任意字符(加上 (?s) 后也匹配换行符) |
\d \w \s | 数字、单词字符 [0-9A-Za-z_]、空白字符(仅 ASCII) |
\D \W \S | 上面各项的取反 |
[abc] [^abc] [a-z] | 字符类 |
\pL \p{Greek} | Unicode 字符类:任意字母、任意希腊字符 |
* + ? {n,m} | 重复,贪婪模式 |
*? +? ?? | 重复,非贪婪模式 |
^ $ | 文本的开头和结尾(加上 (?m) 后为行的开头和结尾) |
\b | 单词边界(ASCII) |
a|b | 选择 |
(?i) | 从该位置到所在分组结尾不区分大小写;通常写在模式开头 |
\d 和 \w 只匹配 ASCII。要匹配“任何语言中的任意字母”,使用 \pL;要匹配任意 Unicode 数字,使用 \p{Nd}。regexp.QuoteMeta(s) 会转义 s 中的所有特殊字符,用用户输入构建模式时需要用到它。
RE2 做不到的事
Go 的 regexp 保证匹配所需的时间与输入长度成线性关系。会破坏这一保证的功能都不受支持:
- 没有先行断言和后行断言:
(?=...)、(?!...)、(?<=...)、(?<!...)都会导致编译错误。由于 Go 1.22 开始接受(?<name>...)形式的命名组,后行断言现在会报出令人困惑的invalid named capture错误,如下面的程序所示。 - 没有反向引用: 无法用
(\w)\1匹配重复的字母。 - 没有占有型量词和原子组。
解决办法几乎总是一样的:多捕获一点内容,然后在 Go 中进行过滤或切片。好处是 Go 的 regexp 不会陷入灾难性回溯。像 (a+)+$ 这样在 PCRE 引擎中面对一个短输入就可能卡死的模式,在这里以线性时间运行,当模式或输入来自外部时,这一点很重要。
常见错误
- 在循环或处理器中编译。 只编译一次,存进包级变量。
- 用双引号写模式。
"\d"甚至不是一个合法的 Go 字符串;请使用反引号。 - 校验时忘记加锚点。
\d{5}会匹配"abc123456xyz"的一部分。应使用^\d{5}$。 - 不检查
nil就对子匹配结果按索引取值。 - 以为
\w或\b能理解非 ASCII 文本。 使用\pL这样的 Unicode 字符类。 - 对固定字符串使用正则。
strings包更清晰也更快。
常见问题
Go 中如何检查字符串是否匹配某个正则?
用 regexp.MustCompile 编译一次模式,并把它写成反引号括起来的原始字符串,这样反斜杠就不需要转义,然后调用 re.MatchString(s)。如果没有锚点(^ 和 $),只要模式在字符串的任何位置匹配,MatchString 就返回 true。
Go 的 regexp 支持先行断言和后行断言吗?
不支持。Go 的 regexp 使用 RE2 语法,它没有先行断言、后行断言和反向引用。作为交换,匹配所需的时间与输入长度成线性关系,所以恶意的模式或输入无法让你的程序卡死。改用捕获组加一点 Go 代码,或者把检查拆成两个正则。
regexp.Compile 和 regexp.MustCompile 有什么区别?
Compile 返回编译后的模式和一个 error。MustCompile 则在模式无效时直接 panic。对于写在源代码中的模式,用 MustCompile,通常放在包级变量里,这样拼写错误会在启动时就暴露出来。对于来自用户或配置文件的模式,用 Compile。
Go 中如何获取正则的捕获组?
使用 FindStringSubmatch,它返回一个 slice,索引 0 是整个匹配,索引 1、2…是各个组;没有匹配时返回 nil。要获取所有匹配,用 FindAllStringSubmatch(s, -1)。命名组 (?P<year>\d{4}) 可以通过 re.SubexpIndex("year") 查到。