
Go 的 regexp 包中,. 默认不匹配换行符,需添加 (?s) 标志(单行模式)才能跨行匹配;(?m)(多行模式)仅影响 ^ 和 $ 的行为,对 . 无效。
go 的 `regexp` 包中,`.` 默认不匹配换行符,需添加 `(?s)` 标志(单行模式)才能跨行匹配;`(?m)`(多行模式)仅影响 `^` 和 `$` 的行为,对 `.` 无效。
在 Go 中处理多行文本的正则匹配时,一个常见误区是误用 (?m)(多行模式)。如以下代码所示:
func main() {
r := regexp.MustCompile(`(?m)<think>(.*)</think>`) // ❌ 错误:(?m) 不启用 . 匹配换行
const s = `That is
<think>
FOOBAR
</think>`
fmt.Printf("%#v\n", r.FindStringSubmatch(s)) // 输出 []byte(nil),匹配失败
}
问题根源在于:Go 的正则引擎遵循 POSIX 兼容规则,. 元字符默认不匹配 \n(换行符),而 (?m) 仅改变 ^ 和 $ 的锚定行为(使其匹配每行起止),并不影响 . 的匹配范围。
✅ 正确做法是启用 (?s) 单行模式(single-line mode),它让 . 可匹配包括换行符在内的任意字符:
func main() {
r := regexp.MustCompile(`(?s)<think>(.*?)</think>`) // ✅ 正确:(?s) 启用 . 匹配 \n;.*? 实现非贪婪匹配
const s = `That is
<think>
FOOBAR
</think>`
matches := r.FindStringSubmatch(s)
if len(matches) > 0 {
fmt.Printf("Matched content: %s\n", strings.TrimSpace(string(matches[1:]))) // 输出: FOOBAR
}
}
⚠️ 注意事项:
- (?s) 是关键——没有它,.* 在遇到换行时即终止匹配;
- 使用 .*?(非贪婪量词)而非 .*,避免跨多个
... 标签过度匹配(例如当文本含多个标签时,.* 会从第一个匹配到最后一个 ); - (?m) 和 (?s) 可同时使用(如 (?ms)),但本例中仅需 (?s);
- 若需严格匹配标签内非空白首尾内容,建议结合 strings.TrimSpace 清理结果,或在正则中用 \s* 显式处理缩进(如
\s*(.*?)\s* )。
总结:Go 多行正则匹配的核心是理解 (?s) 与 (?m) 的语义差异——前者解决 . 的换行能力,后者解决行锚点行为。正确选用 (?s) 并搭配非贪婪匹配,即可稳健提取跨行标签内容。











