Menu

Go 正则表达式 regexp:匹配、查找、子匹配与替换

如何用 regexp 包在 Go 中使用正则表达式:MustCompile、MatchString、FindString 和 FindAllString、捕获组与命名组、ReplaceAllString,以及 RE2 语法的限制,例如不支持后行断言。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

编译一次,然后匹配

把模式写成反引号括起来的原始字符串字面量。在普通的双引号字符串中,每个反斜杠都要写两次("\\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") 查到。

Coddy 编程语言插图

和 Coddy 一起学编程

开始