Компилируйте один раз, потом сопоставляйте
Записывайте шаблоны сырыми строковыми литералами в обратных кавычках. В обычной строке в двойных кавычках каждый обратный слэш нужно удваивать ("\\d+"), и это быстро становится нечитаемым.
regexp.MustCompile паникует, если шаблон некорректен, и для шаблона в исходном коде это именно то, что нужно: опечатка проявляется сразу при запуске, а не на первом запросе. Для шаблонов от пользователей или из конфигурации используйте regexp.Compile и обрабатывайте ошибку:
re, err := regexp.Compile(userPattern)
if err != nil {
return fmt.Errorf("bad pattern: %w", err)
}
Компилируйте один раз. Компиляция гораздо дороже сопоставления. regexp.MustCompile внутри функции, которая выполняется на каждый запрос или строку, каждый раз повторяет эту работу. Кладите скомпилированный *regexp.Regexp в var уровня пакета или в поле структуры. Скомпилированный regexp можно безопасно использовать из многих горутин одновременно.
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] вызовет панику.
(?:...) группирует без захвата, для альтернативы или повторения: (?: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 гарантируют, что сопоставление занимает время, линейное по длине ввода. Возможности, которые нарушили бы эту гарантию, не поддерживаются:
- Нет lookahead и lookbehind:
(?=...),(?!...),(?<=...),(?<!...)дают ошибку компиляции. Поскольку Go 1.22 начал принимать(?<name>...)для именованных групп, lookbehind теперь падает с запутанным сообщениемinvalid named capture, как показывает программа ниже. - Нет обратных ссылок:
(\w)\1для поиска удвоенной буквы невозможен. - Нет притяжательных квантификаторов и атомарных групп.
Обходной путь почти всегда один и тот же: захватить чуть больше, чем нужно, а затем отфильтровать или обрезать в Go. Выигрыш в том, что регулярное выражение Go невозможно загнать в катастрофический перебор с возвратами. Шаблон вроде (a+)+$, который может заморозить движок PCRE на коротком вводе, здесь выполняется за линейное время, и это важно, когда шаблон или ввод приходят извне.
Частые ошибки
- Компиляция в цикле или обработчике. Компилируйте один раз в переменную уровня пакета.
- Шаблоны в двойных кавычках.
"\d"вообще не является корректной строкой Go; используйте обратные кавычки. - Забытые якоря при проверке.
\d{5}совпадает внутри"abc123456xyz". Используйте^\d{5}$. - Обращение к результату с подгруппами без проверки на
nil. - Ожидание, что
\wили\bпонимают не-ASCII текст. Используйте классы Unicode вроде\pL. - regexp для фиксированных строк. Пакет
stringsпонятнее и быстрее.
Часто задаваемые вопросы
Как проверить, что строка соответствует регулярному выражению в Go?
Скомпилируйте шаблон один раз через regexp.MustCompile, записав его сырой строкой в обратных кавычках, чтобы не экранировать обратные слэши, а затем вызовите re.MatchString(s). Без якорей (^ и $) MatchString возвращает true, если шаблон совпадает в любом месте строки.
Поддерживает ли regexp в Go lookahead и lookbehind?
Нет. regexp в Go использует синтаксис RE2, в котором нет lookahead, lookbehind и обратных ссылок. Взамен сопоставление выполняется за время, линейное по размеру ввода, так что враждебный шаблон или ввод не может подвесить программу. Используйте группы захвата и немного кода на Go или разбейте проверку на два регулярных выражения.
Чем regexp.Compile отличается от regexp.MustCompile?
Compile возвращает скомпилированный шаблон и ошибку. MustCompile вместо этого паникует на некорректном шаблоне. Используйте MustCompile для шаблонов, записанных в исходном коде, обычно в переменной уровня пакета, чтобы опечатка проявилась при запуске. Для шаблонов от пользователей или из файлов конфигурации используйте Compile.
Как получить группы захвата из регулярного выражения в Go?
Используйте FindStringSubmatch: он возвращает слайс, где под индексом 0 всё совпадение, а под индексами 1, 2, ... группы, или nil, если совпадения нет. Для всех совпадений используйте FindAllStringSubmatch(s, -1). Именованные группы (?P<year>\d{4}) можно найти через re.SubexpIndex("year").