Golang regexp 包:入门与用法 – wiki基地


Golang regexp 包:入门与用法详解

正则表达式(Regular Expression,简称 Regexp 或 Regex)是处理字符串的强大工具,它使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。无论是在文本搜索、数据提取、输入验证还是文本替换等场景,正则表达式都扮演着至关重要的角色。

Go 语言的标准库提供了一个功能强大且高效的 regexp 包,用于处理正则表达式。与其他语言中的正则表达式引擎不同,Go 的 `regexp 包基于 RE2 库实现,RE2 是一个由 Google 开发的快速、安全、纯粹的正则表达式引擎,它保证了线性时间的匹配速度(相对于输入大小),并且不会出现栈溢出等问题,但也因此牺牲了一些高级特性,例如后向引用(除替换场景)、零宽断言(lookarounds)等。这使得 Go 的正则表达式非常适合处理不受信任的输入,并且在性能要求较高的场景下表现出色。

本文将详细介绍 Go 语言 regexp 包的基础概念、核心功能以及常见用法,帮助读者从入门到熟练掌握 Go 中的正则表达式处理。

1. 正则表达式基础回顾 (Go 的 RE2 语法)

在深入 Go 的 regexp 包之前,我们先简要回顾一下正则表达式的一些基本概念。Go 的 regexp 包遵循 RE2 语法,它与 PCRE (Perl Compatible Regular Expressions) 语法类似,但有一些重要的区别。

以下是一些常用的 RE2 语法元素:

  • 字面字符: 大部分字符都代表其本身,例如 a 匹配字符 ‘a’,1 匹配数字 ‘1’。
  • 元字符 (Metacharacters):
    • .: 匹配除换行符外的任意单个字符。
    • \d: 匹配任意数字 (0-9)。
    • \D: 匹配任意非数字字符。
    • \s: 匹配任意空白字符 (空格、制表符、换行符等)。
    • \S: 匹配任意非空白字符。
    • \w: 匹配任意单词字符 (字母、数字、下划线)。
    • \W: 匹配任意非单词字符。
    • \b: 匹配单词边界。
    • \B: 匹配非单词边界。
  • 锚点 (Anchors):
    • ^: 匹配行的开始(如果使用 MatchString 等方法,也可以匹配字符串的开始)。
    • $: 匹配行的结束(如果使用 MatchString 等方法,也可以匹配字符串的结束)。
    • \A: 匹配字符串的开始。
    • \z: 匹配字符串的结束。
  • 字符集 (Character Sets) / 字符类 (Character Classes):
    • [abc]: 匹配方括号内的任意一个字符。
    • [a-z]: 匹配指定范围内的任意一个字符。
    • [^abc]: 匹配除方括号内的任意一个字符外的所有字符。
  • 量词 (Quantifiers): 控制前面元素的匹配次数。
    • ?: 匹配 0 次或 1 次。
    • *: 匹配 0 次或多次 (贪婪模式)。
    • +: 匹配 1 次或多次 (贪婪模式)。
    • {n}: 精确匹配 n 次。
    • {n,}: 匹配至少 n 次。
    • {n,m}: 匹配至少 n 次,但不超过 m 次。
  • 非贪婪量词 (Non-greedy Quantifiers): 在量词后加 ? 使其变为非贪婪模式,匹配尽可能少的次数。
    • ??: 匹配 0 次或 1 次 (非贪婪)。
    • *?: 匹配 0 次或多次 (非贪婪)。
    • +?: 匹配 1 次或多次 (非贪婪)。
    • {n,m}?: 匹配 n 到 m 次 (非贪婪)。
  • 分组与捕获 (Grouping and Capturing):
    • (...): 将括号内的表达式视为一个整体,并捕获匹配到的内容 (创建捕获组)。
    • (?:...): 非捕获分组,只分组不捕获。
  • 选择 (Alternation):
    • |: 匹配 | 左边或右边的表达式。
  • 转义: 使用 \ 转义元字符,使其匹配字面意义。例如 \. 匹配字面上的点号,\\ 匹配字面上的反斜杠。

RE2 与 PCRE 的主要区别 (Go regexp 的特点):

  • 无后向引用 (Backreferences) 用于匹配: RE2 不支持在模式中使用 \1, \2 等来引用前面捕获组匹配的内容进行匹配。但 支持 在替换字符串中使用 $1, $2 等进行替换。
  • 无零宽断言 (Lookarounds): RE2 不支持 (?=...), (?!...), (?<=...), (?<!...) 等前向/后向零宽断言。
  • 无递归模式: 不支持 (?R).
  • 无条件匹配: 不支持 (?(condition)yes-pattern|no-pattern).
  • 无注释模式: 不支持 (?#comment).
  • 保证线性时间: 这是 RE2 的核心优势,不受输入结构影响,匹配时间总是与输入大小成正比。

理解这些基础和 Go regexp 基于 RE2 的特点,对于高效使用 Go 正则表达式至关重要。

2. Go regexp 包核心类型与编译

Go regexp 包的核心是 Regexp 类型。一个 Regexp 类型的对象代表一个已编译的正则表达式。在进行匹配、查找或替换操作之前,必须先将正则表达式字符串编译成 Regexp 对象。

编译正则表达式有两个主要函数:

  • regexp.Compile(expr string) (*Regexp, error): 编译正则表达式 expr。如果表达式无效,它会返回一个 *Regexp 对象和相应的错误。这是推荐的编译方式,因为它可以让你妥善处理可能出现的编译错误。
  • regexp.MustCompile(expr string) *Regexp: 编译正则表达式 expr。如果表达式无效,它会发生 panic。这个函数通常用于在全局范围或 init() 函数中编译常量正则表达式,因为在程序启动时如果正则表达式是无效的,让程序崩溃是合理的处理方式。

为什么需要编译?

编译过程会解析正则表达式字符串,并将其转换成内部的高效表示形式,以便进行匹配操作。如果频繁地使用同一个正则表达式,但每次都使用字符串形式(例如 regexp.MatchString("pattern", text)),每次调用时都会隐含地重新编译。这会带来不必要的开销。将正则表达式编译一次,然后复用编译后的 Regexp 对象,可以显著提高性能。

示例:编译正则表达式

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
// 使用 Compile 编译,需要处理错误
pattern := ^hello, (.*?)$
re, err := regexp.Compile(pattern)
if err != nil {
fmt.Printf(“编译正则表达式出错: %v\n”, err)
return
}
fmt.Printf(“正则表达式 \”%s\” 编译成功\n”, pattern)

// 使用 MustCompile 编译,用于确定有效的常量表达式
// 注意:这个会 panic 如果表达式无效,所以通常在 init() 或全局使用
pattern2 := `\d+` // 匹配一个或多个数字
re2 := regexp.MustCompile(pattern2)
fmt.Printf("正则表达式 \"%s\" 编译成功 (使用 MustCompile)\n", pattern2)

// 编译一个无效的正则表达式会触发 panic (如果使用 MustCompile)
// pattern3 := `(` // 非法表达式
// re3 := regexp.MustCompile(pattern3) // 这行会 panic
// fmt.Println(re3)

// 如果使用 Compile,可以捕获错误
pattern4 := `(`
re4, err := regexp.Compile(pattern4)
if err != nil {
    fmt.Printf("编译无效正则表达式 \"%s\" 时捕获到错误: %v\n", pattern4, err)
} else {
    fmt.Println(re4) // 不会执行到这里
}

}
“`

最佳实践:

  • 如果你的正则表达式是一个在编译时就已知的常量,并且你确定它是有效的,使用 regexp.MustCompile 并将其存储在一个全局变量或包级别的变量中。
  • 如果你的正则表达式是动态生成的,或者来源于外部输入(例如用户输入或配置文件),使用 regexp.Compile 并检查返回的错误,确保程序不会因为无效的正则表达式而崩溃。
  • 避免在紧密的循环内部重复调用 regexp.Compile 或使用那些隐含编译的函数。

3. 匹配字符串 (Match 系列方法)

Regexp 对象提供了多种方法来检查一个字符串是否符合正则表达式模式。

  • func (re *Regexp) Match(b []byte) bool: 检查字节切片 b 是否完全匹配正则表达式。注意这里的“完全匹配”是指 ^$ 锚点隐含在模式两端,即模式必须匹配整个字节切片。
  • func (re *Regexp) MatchString(s string) bool: 检查字符串 s 是否完全匹配正则表达式。同样隐含 ^$ 锚点。这是最常用的简单匹配方法。
  • func Match(pattern string, b []byte) (matched bool, err error): 这是一个方便函数,它会先编译 pattern 然后调用 Match(b). 每次调用都会重新编译。如果只需要简单检查一次且不关心性能(或性能要求不高),可以使用它。
  • func MatchString(pattern string, s string) (matched bool, err error): 类似于 Match,但接受字符串参数。同样每次调用都会重新编译。

示例:使用 MatchString

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
// 匹配电子邮件地址的基本模式
emailPattern := ^\w+@\w+\.\w+$
reEmail := regexp.MustCompile(emailPattern)

testEmails := []string{
    "[email protected]", // 匹配
    "[email protected]", // 匹配
    "invalid-email",    // 不匹配
    "@domain.com",      // 不匹配
    "[email protected]",        // 不匹配
    "user@domain.",     // 不匹配
}

fmt.Printf("正则表达式: \"%s\"\n", emailPattern)
for _, email := range testEmails {
    matched := reEmail.MatchString(email)
    fmt.Printf("'%s' 匹配? %t\n", email, matched)
}

fmt.Println("\n使用方便函数 MatchString (注意隐含编译开销):")
matched, err := regexp.MatchString(`go+`, "goooo")
if err != nil {
    fmt.Println("编译错误:", err)
    return
}
fmt.Printf("'goooo' 匹配 'go+'? %t\n", matched)

matched, err = regexp.MatchString(`go+`, "abcgo")
if err != nil {
    fmt.Println("编译错误:", err)
    return
}
// 注意:这里返回 true,因为方便函数 MatchString 行为与编译后的 MatchString 不同
//方便函数 MatchString 查找字符串中是否存在匹配的部分,而不是要求整个字符串匹配
// 这是一个重要的区别!方便函数 Match* 的行为更像 Find* 但只返回 bool
// 通常建议使用编译后的 MatchString 来检查整个字符串匹配
fmt.Printf("'abcgo' 匹配 'go+'? %t\n", matched)

// 澄清编译后的 MatchString 行为
reGo := regexp.MustCompile(`go+`)
fmt.Println("\n使用编译后的 MatchString (要求整个字符串匹配):")
fmt.Printf("'goooo' 匹配 'go+'? %t\n", reGo.MatchString("goooo")) // true
fmt.Printf("'abcgo' 匹配 'go+'? %t\n", reGo.MatchString("abcgo")) // false
fmt.Printf("'goooodefg' 匹配 'go+'? %t\n", reGo.MatchString("goooodefg")) // false

}
“`

重要提示: 方便函数 regexp.MatchStringregexp.Match 的行为与 *Regexp.MatchString*Regexp.Match 不同。方便函数是在输入字符串中查找是否存在 任何 匹配子串,而编译后的方法要求正则表达式 完全 匹配输入字符串(如同模式前后有 ^$ 锚点)。为了避免混淆和提高性能,强烈建议先编译正则表达式,然后使用 *Regexp 对象的方法。如果只想检查子串是否存在,应该使用 Find 系列方法(例如 FindStringIndex)并检查结果是否为 nil。

4. 查找匹配项 (Find 系列方法)

当我们需要找到字符串中与正则表达式匹配的具体内容时,可以使用 Find 系列方法。这些方法会扫描输入字符串,找到第一个或所有匹配的子串。

  • func (re *Regexp) Find(b []byte) []byte: 在字节切片 b 中查找第一个匹配项,并返回匹配的字节切片。如果没有找到匹配项,返回 nil
  • func (re *Regexp) FindString(s string) string: 在字符串 s 中查找第一个匹配项,并返回匹配的字符串。如果没有找到匹配项,返回空字符串 ""。注意:返回空字符串可能是因为没有匹配,也可能是因为匹配到了一个空字符串(例如模式 a| 匹配 “a” 或空字符串)。使用 FindIndexFindStringIndex 并检查返回是否为 nil 是更安全的判断是否有匹配项的方式。
  • func (re *Regexp) FindAll(b []byte, n int) [][]byte: 在字节切片 b 中查找所有非重叠的匹配项。n 参数指定最多查找多少个匹配项。如果 n < 0,则查找所有匹配项。返回一个字节切片的切片。
  • func (re *Regexp) FindAllString(s string, n int) []string: 在字符串 s 中查找所有非重叠的匹配项,返回字符串切片。n 参数的含义同上。

示例:查找匹配项

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
text := “Call me at 123-456-7890 or 987.654.3210. Also try 555-111-2222.”
// 匹配电话号码的简单模式 (数字-数字-数字 或 数字.数字.数字)
phonePattern := \d{3}[-.]\d{3}[-.]\d{4}
rePhone := regexp.MustCompile(phonePattern)

// 查找第一个匹配项
firstMatch := rePhone.FindString(text)
fmt.Printf("文本: \"%s\"\n", text)
fmt.Printf("模式: \"%s\"\n", phonePattern)
fmt.Printf("第一个匹配项: '%s'\n", firstMatch) // 输出: '123-456-7890'

// 查找所有匹配项
allMatches := rePhone.FindAllString(text, -1) // -1 表示查找所有
fmt.Printf("所有匹配项: %v\n", allMatches) // 输出: [123-456-7890 987.654.3210 555-111-2222]

// 查找前2个匹配项
twoMatches := rePhone.FindAllString(text, 2)
fmt.Printf("前2个匹配项: %v\n", twoMatches) // 输出: [123-456-7890 987.654.3210]

// 如果没有匹配项
textNoPhone := "No phone numbers here."
noMatch := rePhone.FindString(textNoPhone)
fmt.Printf("在 \"%s\" 中查找: '%s' (空字符串表示未找到)\n", textNoPhone, noMatch)

allNoMatch := rePhone.FindAllString(textNoPhone, -1)
fmt.Printf("在 \"%s\" 中查找所有: %v (空切片表示未找到)\n", textNoPhone, allNoMatch)

// 处理匹配空字符串的情况 (FindString 返回 "")
reEmptyMatch := regexp.MustCompile(`a*`) // 匹配0个或多个a
textWithEmpty := "bbb"
emptyMatch := reEmptyMatch.FindString(textWithEmpty) // 会匹配开头的空字符串
fmt.Printf("模式 \"a*\" 在 \"%s\" 中查找第一个: '%s'\n", textWithEmpty, emptyMatch) // 输出: ''
// 为了判断是否有匹配,检查索引更安全
emptyMatchIndex := reEmptyMatch.FindStringIndex(textWithEmpty)
fmt.Printf("模式 \"a*\" 在 \"%s\" 中查找第一个索引: %v (非 nil 表示有匹配)\n", textWithEmpty, emptyMatchIndex) // 输出: [0 0]

}
“`

5. 使用子匹配 (捕获组)

正则表达式中的圆括号 () 创建捕获组 (capturing group),它们可以捕获匹配到的文本中的特定部分。Go 的 regexp 包提供了方法来访问这些捕获组的匹配内容,也称为子匹配 (submatch)。

  • func (re *Regexp) FindSubmatch(b []byte) [][]byte: 查找第一个匹配项,并返回一个字节切片的切片。返回的切片中,第一个元素 ([0]) 是整个匹配项,后续元素 ([1], [2], …) 对应于正则表达式中从左到右的捕获组的匹配内容。如果没有匹配项,返回 nil
  • func (re *Regexp) FindStringSubmatch(s string) []string: 查找第一个匹配项及其子匹配,返回字符串切片。结构同上。
  • func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte: 查找所有匹配项及其子匹配。返回 [][][]byte
  • func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string: 查找所有匹配项及其子匹配,返回 [][]string

示例:使用子匹配

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
text := “Name: Alice, Age: 30; Name: Bob, Age: 25.”
// 模式:匹配 “Name: ” 后面的名字 和 ” Age: ” 后面的年龄
// (.*?) 是非贪婪匹配任意字符,用于捕获名字
// (\d+) 是匹配一个或多个数字,用于捕获年龄
pattern := Name: (.*?),\s*Age: (\d+)
re := regexp.MustCompile(pattern)

// 查找第一个匹配项及其子匹配
firstMatchAndSubmatches := re.FindStringSubmatch(text)
if firstMatchAndSubmatches != nil {
    fmt.Println("第一个匹配项及子匹配:")
    fmt.Printf("  整个匹配: '%s'\n", firstMatchAndSubmatches[0]) // Index 0 是整个匹配项
    fmt.Printf("  名字 (子匹配 1): '%s'\n", firstMatchAndSubmatches[1]) // Index 1 是第一个捕获组 (...)
    fmt.Printf("  年龄 (子匹配 2): '%s'\n", firstMatchAndSubmatches[2]) // Index 2 是第二个捕获组 (...)
} else {
    fmt.Println("未找到匹配项")
}
// 输出:
// 第一个匹配项及子匹配:
//   整个匹配: 'Name: Alice, Age: 30'
//   名字 (子匹配 1): 'Alice'
//   年龄 (子匹配 2): '30'


fmt.Println("\n查找所有匹配项及子匹配:")
// 查找所有匹配项及其子匹配
allMatchesAndSubmatches := re.FindAllStringSubmatch(text, -1)
if allMatchesAndSubmatches != nil {
    for i, match := range allMatchesAndSubmatches {
        fmt.Printf("匹配 #%d:\n", i+1)
        fmt.Printf("  整个匹配: '%s'\n", match[0])
        fmt.Printf("  名字 (子匹配 1): '%s'\n", match[1])
        fmt.Printf("  年龄 (子匹配 2): '%s'\n", match[2])
    }
} else {
    fmt.Println("未找到匹配项")
}
// 输出:
// 查找所有匹配项及子匹配:
// 匹配 #1:
//   整个匹配: 'Name: Alice, Age: 30'
//   名字 (子匹配 1): 'Alice'
//   年龄 (子匹配 2): '30'
// 匹配 #2:
//   整个匹配: 'Name: Bob, Age: 25'
//   名字 (子匹配 1): 'Bob'
//   年龄 (子匹配 2): '25'

// 如果某个可选的捕获组没有匹配到,它对应的子匹配将是空字符串。
patternOptional := `(a)?(b)`
reOptional := regexp.MustCompile(patternOptional)
textOptional := "b"
matchOptional := reOptional.FindStringSubmatch(textOptional)
fmt.Printf("\n模式 \"%s\" 在 \"%s\" 中查找:\n", patternOptional, textOptional)
if matchOptional != nil {
    fmt.Printf("  整个匹配: '%s'\n", matchOptional[0]) // b
    fmt.Printf("  子匹配 1 (a?): '%s'\n", matchOptional[1]) // "" (因为a没有匹配)
    fmt.Printf("  子匹配 2 (b): '%s'\n", matchOptional[2]) // b
}

}
“`

命名捕获组 (Named Capturing Groups)

Go 的 regexp 包(基于 RE2)支持命名捕获组,语法是 (?P<name>...)。这使得通过名称而不是索引来引用捕获组成为可能,提高了代码的可读性。

Regexp 类型提供了两个方法来处理命名捕获组:

  • func (re *Regexp) SubexpNames() []string: 返回捕获组的名称切片。索引 0 对应整个匹配项(名称为空字符串),后续索引对应各个捕获组的名称。如果捕获组没有指定名称,则其名称为空字符串。
  • func (re *Regexp) SubexpIndex(name string) int: 返回给定名称的捕获组的索引。如果找不到该名称的捕获组,返回 -1。

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
text := “user_id: 1001, user_name: Alice”
// 使用命名捕获组
pattern := user_id: (?P<id>\d+), user_name: (?P<name>\w+)
re := regexp.MustCompile(pattern)

match := re.FindStringSubmatch(text)
if match != nil {
    fmt.Println("使用命名捕获组查找:")
    fmt.Printf("整个匹配: '%s'\n", match[0])

    // 获取捕获组名称
    names := re.SubexpNames()
    fmt.Printf("捕获组名称: %v\n", names) // 输出: [ id name] (索引0为空名称)

    // 通过名称访问匹配内容
    // 找到 'id' 捕获组的索引
    idIndex := re.SubexpIndex("id")
    if idIndex >= 0 && idIndex < len(match) {
        fmt.Printf("ID (通过名称): '%s'\n", match[idIndex])
    } else {
        fmt.Println("未找到 'id' 捕获组")
    }

    // 找到 'name' 捕获组的索引
    nameIndex := re.SubexpIndex("name")
    if nameIndex >= 0 && nameIndex < len(match) {
        fmt.Printf("Name (通过名称): '%s'\n", match[nameIndex])
    } else {
        fmt.Println("未找到 'name' 捕获组")
    }
} else {
    fmt.Println("未找到匹配项")
}
// 输出:
// 使用命名捕获组查找:
// 整个匹配: 'user_id: 1001, user_name: Alice'
// 捕获组名称: [ id name]
// ID (通过名称): '1001'
// Name (通过名称): 'Alice'

}
“`

命名捕获组虽然不能在 匹配 时进行后向引用,但在提取匹配内容和进行 替换 时非常有用。

6. 获取匹配项的索引 (FindIndex 系列方法)

除了获取匹配的内容,有时我们还需要知道匹配项在原始字符串中的起始和结束位置(字节索引)。Regexp 包提供了相应的 FindIndex 系列方法。

这些方法返回一个 []int 切片,通常包含两个元素 [start, end),表示匹配项的起始(包含)和结束(不包含)字节索引。对于包含子匹配的方法,返回的 []int 切片长度将是捕获组数量的两倍,每个捕获组对应一对 [start, end) 索引。

  • func (re *Regexp) FindIndex(b []byte) []int: 查找第一个匹配项,返回 [start, end) 字节索引。如果没有找到,返回 nil
  • func (re *Regexp) FindStringIndex(s string) []int: 查找第一个匹配项,返回 [start, end) 字节索引。如果没有找到,返回 nil
  • func (re *Regexp) FindSubmatchIndex(b []byte) []int: 查找第一个匹配项及其子匹配的索引。返回 [fullMatchStart, fullMatchEnd, submatch1Start, submatch1End, ...]. 如果没有找到,返回 nil
  • func (re *Regexp) FindStringSubmatchIndex(s string) []int: 查找第一个匹配项及其子匹配的索引,返回 []int
  • func (re *Regexp) FindAllIndex(b []byte, n int) [][]int: 查找所有匹配项的索引。返回 [][]int
  • func (re *Regexp) FindAllStringIndex(s string, n int) [][]int: 查找所有匹配项的索引,返回 [][]int
  • func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][][]int: 查找所有匹配项及其子匹配的索引。返回 [][][]int
  • func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][][]int: 查找所有匹配项及其子匹配的索引,返回 [][][]int

示例:获取匹配项索引

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
text := “abc 123 def 456 ghi”
// 匹配数字
pattern := \d+
re := regexp.MustCompile(pattern)

// 查找第一个匹配项的索引
firstIndex := re.FindStringIndex(text)
fmt.Printf("文本: \"%s\"\n", text)
fmt.Printf("模式: \"%s\"\n", pattern)
fmt.Printf("第一个匹配项索引: %v\n", firstIndex) // 输出: [4 7] (对应 "123")
if firstIndex != nil {
    fmt.Printf("提取第一个匹配项: '%s'\n", text[firstIndex[0]:firstIndex[1]]) // 使用索引切片
}

// 查找所有匹配项的索引
allIndices := re.FindAllStringIndex(text, -1)
fmt.Printf("所有匹配项索引: %v\n", allIndices) // 输出: [[4 7] [12 15]] (对应 "123" 和 "456")
if allIndices != nil {
    fmt.Println("提取所有匹配项:")
    for _, indexPair := range allIndices {
        fmt.Printf("  '%s' (索引 %v)\n", text[indexPair[0]:indexPair[1]], indexPair)
    }
}

fmt.Println("\n使用子匹配索引:")
textWithSubmatch := "Key=Value, Name=Alice"
// 模式:匹配 "Key=" 或 "Name=" 后面的值
patternWithSubmatch := `(Key|Name)=(.*)`
reWithSubmatch := regexp.MustCompile(patternWithSubmatch)

// 查找第一个匹配项及其子匹配的索引
firstSubmatchIndex := reWithSubmatch.FindStringSubmatchIndex(textWithSubmatch)
fmt.Printf("文本: \"%s\"\n", textWithSubmatch)
fmt.Printf("模式: \"%s\"\n", patternWithSubmatch)
fmt.Printf("第一个匹配项及其子匹配索引: %v\n", firstSubmatchIndex)
// 输出: [0 9 0 3 4 9]
// 解释:
// [0 9]: 整个匹配项 "Key=Value" 的索引
// [0 3]: 第一个捕获组 "(Key|Name)" 匹配 "Key" 的索引
// [4 9]: 第二个捕获组 "(.*)" 匹配 "Value" 的索引

if firstSubmatchIndex != nil && len(firstSubmatchIndex) >= 4 { // 至少有整个匹配和第一个子匹配
    // 整个匹配
    fmt.Printf("  整个匹配: '%s'\n", textWithSubmatch[firstSubmatchIndex[0]:firstSubmatchIndex[1]])
    // 第一个子匹配 (Key或Name)
    fmt.Printf("  子匹配 1: '%s'\n", textWithSubmatch[firstSubmatchIndex[2]:firstSubmatchIndex[3]])
    // 第二个子匹配 (Value)
    fmt.Printf("  子匹配 2: '%s'\n", textWithSubmatch[firstSubmatchIndex[4]:firstSubmatchIndex[5]])
}

fmt.Println("\n查找所有匹配项及其子匹配的索引:")
allSubmatchIndices := reWithSubmatch.FindAllStringSubmatchIndex(textWithSubmatch, -1)
fmt.Printf("所有匹配项及其子匹配索引: %v\n", allSubmatchIndices)
// 输出: [[0 9 0 3 4 9] [11 21 11 15 16 21]]

if allSubmatchIndices != nil {
    fmt.Println("提取所有匹配项及其子匹配:")
    for i, matchIndices := range allSubmatchIndices {
        fmt.Printf("匹配 #%d (索引 %v):\n", i+1, matchIndices)
        if len(matchIndices) >= 6 {
            fmt.Printf("  整个匹配: '%s'\n", textWithSubmatch[matchIndices[0]:matchIndices[1]])
            fmt.Printf("  子匹配 1: '%s'\n", textWithSubmatch[matchIndices[2]:matchIndices[3]])
            fmt.Printf("  子匹配 2: '%s'\n", textWithSubmatch[matchIndices[4]:matchIndices[5]])
        }
    }
}

}
“`

获取索引对于需要对匹配到的子串在原始文本中进行定位、高亮或者进一步处理(例如从原始字节切片或字符串中提取子串)的场景非常有用。

7. 替换匹配项 (ReplaceAll 系列方法)

正则表达式最常见的用途之一就是替换文本。Regexp 包提供了强大的替换功能,不仅可以替换匹配的文本,还可以使用捕获组的内容构造替换字符串。

  • func (re *Regexp) ReplaceAll(src, repl []byte) []byte: 将字节切片 src 中所有与正则表达式匹配的部分替换为字节切片 repl。在 repl 中可以使用 $1, $2, ${name} 等来引用捕获组。
  • func (re *Regexp) ReplaceAllString(src, repl string) string: 将字符串 src 中所有与正则表达式匹配的部分替换为字符串 repl。同样支持捕获组引用。这是最常用的替换方法。
  • func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte: 将字节切片 src 中所有与正则表达式匹配的部分替换为调用函数 repl 的结果。repl 函数接收匹配到的字节切片作为参数,返回用于替换的字节切片。这提供了更灵活的动态替换方式。
  • func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string: 类似于 ReplaceAllFunc,但处理字符串。

替换字符串语法 ($):

ReplaceAllReplaceAllStringrepl 参数中,可以使用特殊的 $1, $2 等语法引用捕获组:

  • $0: 匹配整个正则表达式的文本。
  • $1, $2, …: 对应第 1 个、第 2 个等捕获组匹配的文本。
  • ${name}$name: 对应名为 name 的捕获组匹配的文本。
  • $$: 字面意义的美元符号 $.

如果引用的捕获组不存在或未匹配到,$1$name 会被替换为空字符串。

示例:替换匹配项

“`go
package main

import (
“fmt”
“regexp”
“strings” // 用于更简单的替换示例对比
)

func main() {
text := “Date: 2023-10-27, Time: 15:30:00”
// 匹配日期格式 YYYY-MM-DD
datePattern := (\d{4})-(\d{2})-(\d{2})
reDate := regexp.MustCompile(datePattern)

// 将日期格式从 YYYY-MM-DD 转换为 MM/DD/YYYY
// $1: 年, $2: 月, $3: 日
// 替换为 "$2/\/\"
replacedText := reDate.ReplaceAllString(text, "$2/\/\")
fmt.Printf("原始文本: \"%s\"\n", text)
fmt.Printf("模式: \"%s\"\n", datePattern)
fmt.Printf("替换模式: \"%s\"\n", "$2/\/\")
fmt.Printf("替换结果: \"%s\"\n", replacedText)
// 输出: 原始文本: "Date: 2023-10-27, Time: 15:30:00"
//       模式: "(\d{4})-(\d{2})-(\d{2})"
//       替换模式: "$2/\/\"
//       替换结果: "Date: 10/27/2023, Time: 15:30:00"

// 替换所有匹配项,并使用整个匹配项
textNumbers := "Item 123, Price 4.56, Count 789"
numberPattern := `\d+`
reNumbers := regexp.MustCompile(numberPattern)
// 将所有数字用括号括起来
replacedNumbers := reNumbers.ReplaceAllString(textNumbers, "($0)") // $0 代表整个匹配项
fmt.Printf("\n原始文本: \"%s\"\n", textNumbers)
fmt.Printf("模式: \"%s\"\n", numberPattern)
fmt.Printf("替换模式: \"%s\"\n", "($0)")
fmt.Printf("替换结果: \"%s\"\n", replacedNumbers)
// 输出: 原始文本: "Item 123, Price 4.56, Count 789"
//       模式: "\d+"
//       替换模式: "($0)"
//       替换结果: "Item (123), Price (4), Count (789)"
// 注意:4.56 中的 . 没有被匹配,所以只匹配了 4

// 替换包含小数的数字
decimalPattern := `\d+(\.\d+)?` // 匹配整数或小数
reDecimal := regexp.MustCompile(decimalPattern)
replacedDecimal := reDecimal.ReplaceAllString(textNumbers, "[[$0]]")
fmt.Printf("\n原始文本: \"%s\"\n", textNumbers)
fmt.Printf("模式: \"%s\"\n", decimalPattern)
fmt.Printf("替换模式: \"%s\"\n", "[[$0]]")
fmt.Printf("替换结果: \"%s\"\n", replacedDecimal)
// 输出: 原始文本: "Item 123, Price 4.56, Count 789"
//       模式: "\d+(\.\d+)?"
//       替换模式: "[[$0]]"
//       替换结果: "Item [[123]], Price [[4.56]], Count [[789]]"

// 使用命名捕获组进行替换
textNamed := "user: john.doe, id: 999"
namedPattern := `user: (?P<username>[^,]+), id: (?P<userid>\d+)`
reNamed := regexp.MustCompile(namedPattern)
// 替换为 "ID: ${userid}, Username: ${username}"
replacedNamed := reNamed.ReplaceAllString(textNamed, "ID: ${userid}, Username: ${username}")
fmt.Printf("\n原始文本: \"%s\"\n", textNamed)
fmt.Printf("模式: \"%s\"\n", namedPattern)
fmt.Printf("替换模式: \"%s\"\n", "ID: ${userid}, Username: ${username}")
fmt.Printf("替换结果: \"%s\"\n", replacedNamed)
// 输出: 原始文本: "user: john.doe, id: 999"
//       模式: "user: (?P<username>[^,]+), id: (?P<userid>\d+)"
//       替换模式: "ID: ${userid}, Username: ${username}"
//       替换结果: "ID: 999, Username: john.doe"

// 替换为一个字面意义的美元符号 "$"
textDollar := "Price is $20"
reDollar := regexp.MustCompile(`\$20`)
// 将 "$20" 替换为 "$$" + "20" -> "$20" (字面 $)
// 将 "$20" 替换为 "成本 $$20" -> "成本 $20"
replacedDollar := reDollar.ReplaceAllString(textDollar, "成本 $$20")
fmt.Printf("\n原始文本: \"%s\"\n", textDollar)
fmt.Printf("模式: \"%s\"\n", `\$20`)
fmt.Printf("替换模式: \"%s\"\n", "成本 $$20")
fmt.Printf("替换结果: \"%s\"\n", replacedDollar)
// 输出: 原始文本: "Price is $20"
//       模式: "\$20"
//       替换模式: "成本 $$20"
//       替换结果: "Price is 成本 $20"


// 使用 ReplaceAllStringFunc 进行动态替换
textFunc := "Items: apple 5, banana 10, orange 3"
itemPattern := `(\w+)\s+(\d+)` // 捕获物品名称和数量
reFunc := regexp.MustCompile(itemPattern)

// 定义一个替换函数,将数量翻倍并格式化输出
replFunc := func(match string) string {
    // match 参数是整个匹配到的字符串 (例如 "apple 5")
    // 需要再次使用正则表达式或分割来获取子匹配
    submatches := reFunc.FindStringSubmatch(match)
    if len(submatches) > 2 {
        item := submatches[1] // 物品名称
        countStr := submatches[2] // 数量字符串
        var count int
        fmt.Sscan(countStr, &count) // 将数量字符串转为整数
        doubledCount := count * 2
        return fmt.Sprintf("%s %d (original: %d)", item, doubledCount, count) // 格式化输出
    }
    return match // 如果没有子匹配,返回原始匹配
}

replacedFunc := reFunc.ReplaceAllStringFunc(textFunc, replFunc)
fmt.Printf("\n原始文本: \"%s\"\n", textFunc)
fmt.Printf("模式: \"%s\"\n", itemPattern)
fmt.Printf("替换函数逻辑: 将数量翻倍并在括号中显示原数量\n")
fmt.Printf("替换结果 (使用 ReplaceAllStringFunc): \"%s\"\n", replacedFunc)
// 输出: 原始文本: "Items: apple 5, banana 10, orange 3"
//       模式: "(\w+)\s+(\d+)"
//       替换函数逻辑: 将数量翻倍并在括号中显示原数量
//       替换结果 (使用 ReplaceAllStringFunc): "Items: apple 10 (original: 5), banana 20 (original: 10), orange 6 (original: 3)"

}
“`

ReplaceAllStringFuncReplaceAllFunc 提供了极大的灵活性,允许根据匹配到的内容执行任意 Go 代码来生成替换字符串。

8. 高级主题与最佳实践

  • RE2 的优势: 线性时间性能保证是 RE2 的最大亮点。这意味着无论你的输入有多大,或者你的正则表达式有多复杂(只要它符合 RE2 语法),匹配时间都与输入大小成线性关系。这避免了传统回溯型正则表达式引擎在某些模式和输入下可能出现的灾难性回溯,导致性能呈指数级下降甚至服务拒绝 (ReDoS)。虽然缺少一些高级特性,但在大多数实际应用中,RE2 的功能已经足够强大,并且其性能和安全性优势使其成为处理不可信输入的理想选择。
  • 线程安全: Regexp 对象是线程安全的。一旦编译完成,多个 goroutine 可以并发地调用同一个 Regexp 对象的方法而无需额外的同步措施。这是 Go 标准库设计的优点之一。
  • 编译时机: 重申编译的重要性。对于会重复使用的正则表达式,务必将其编译一次并存储起来复用。在 init() 函数或作为全局/包级变量使用 regexp.MustCompile 是常见且高效的模式。
  • 避免不必要的正则表达式: 对于简单的字符串查找、前缀/后缀检查、分割等操作,优先考虑使用 strings 包提供的函数(如 strings.Contains, strings.HasPrefix, strings.Split 等),它们通常比正则表达式更简洁、易读且性能更高。只有当需要进行复杂的模式匹配时,才应该使用正则表达式。
  • 性能考虑: 如果只需要判断是否存在匹配,使用 re.MatchString() 通常比 re.FindString() 更快,因为 MatchString 可以在找到第一个匹配时就停止,而 FindString 需要找到并返回具体的匹配内容。
  • 字节切片 vs. 字符串: regexp 包提供了操作 []bytestring 的两套方法。对于大多数文本处理,使用字符串方法 (FindString, ReplaceAllString 等) 更方便。但如果处理大量二进制数据或者对性能有极致要求,直接操作字节切片可能略有优势。

9. 总结

Go 语言的 regexp 包是处理字符串模式匹配、查找和替换的强大工具。它基于高效安全的 RE2 引擎,提供了简洁易用的 API。

本文详细介绍了 regexp 包的核心用法:

  • 编译: 使用 regexp.Compileregexp.MustCompile 将正则表达式字符串编译为可复用的 Regexp 对象。
  • 匹配: 使用 MatchString 等方法快速检查字符串是否完全匹配模式。
  • 查找: 使用 FindStringFindAllString 等方法找到匹配的子串。
  • 子匹配: 使用 FindStringSubmatchFindAllStringSubmatch 等方法提取捕获组的内容,包括对命名捕获组的支持。
  • 索引: 使用 FindStringIndexFindStringSubmatchIndex 等方法获取匹配项在原始字符串中的位置信息。
  • 替换: 使用 ReplaceAllString 进行基于模式的替换,支持使用 $1, $name 等引用捕获组,或者使用 ReplaceAllStringFunc 实现动态替换。

理解 Go regexp 包基于 RE2 的特性(线性时间、无后向引用匹配等)以及掌握编译和复用 Regexp 对象的技巧,将帮助你高效、安全地在 Go 程序中处理各种复杂的字符串模式匹配任务。

10. 参考资料

希望这篇详细的文章能帮助你全面了解和掌握 Golang regexp 包的使用!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部