Expressões regulares em Go (Golang): um guia detalhado
Что это такое и зачем? (аналогия из реальной жизни)
Представьте, что вы — библиотекарь в огромной библиотеке, где миллионы книг. К вам приходит читатель и говорит: «Найдите мне все книги, где на 42-й странице есть слово "дракон", но только если оно написано с большой буквы и стоит в конце предложения». Без специальных инструментов вам пришлось бы перелистывать каждую книгу вручную — это заняло бы годы.
Регулярные выражения (Regular Expressions, или сокращённо regexp) — это как раз тот самый волшебный инструмент, который позволяет описать сложный текстовый шаблон одной короткой строкой. В мире программирования это незаменимая вещь для:
- Поиска — найти все email-адреса в тексте
- Проверки — убедиться, что пользователь ввёл правильный номер телефона
- Замены — заменить все даты из формата "2024-01-15" на "15.01.2024"
- Разделения — разбить строку на части по сложному разделителю
В Go (Golang) работа с регулярными выражениями вынесена в стандартный пакет regexp. Это значит, что вам не нужно устанавливать никаких дополнительных библиотек — всё уже есть в языке.
Важно: Регулярные выражения — это мощный, но опасный инструмент. Как говорится в старой поговорке программистов: «У вас есть проблема. Вы решили использовать регулярные выражения. Теперь у вас две проблемы». Используйте их с умом!
Теория: как это работает изнутри
Чтобы понять, как работают регулярные выражения, давайте разберём их на составные части. Представьте, что регулярное выражение — это инструкция для робота-поисковика, который ползёт по тексту символ за символом.
Основные строительные блоки
| Символ | Что означает | Пример | Найдёт |
|---|---|---|---|
. |
Любой одиночный символ (кроме перевода строки) | к.т |
"кот", "кит", "к?т" |
* |
Предыдущий символ 0 или более раз | ко*т |
"кт", "кот", "коот", "кооот" |
+ |
Предыдущий символ 1 или более раз | ко+т |
"кот", "коот", но НЕ "кт" |
? |
Предыдущий символ 0 или 1 раз | ко?т |
"кт" и "кот" |
^ |
Начало строки | ^Привет |
"Привет" только в начале строки |
$ |
Конец строки | конец$ |
"конец" только в конце строки |
[] |
Набор символов (любой из перечисленных) | [км]от |
"кот" и "мот" |
[^] |
Отрицание набора | [^к]от |
"рот", "бот", но НЕ "кот" |
| |
ИЛИ (логическое) | кот|пёс |
"кот" или "пёс" |
() |
Группировка | (ко)+т |
"кот", "кокот", "кококот" |
Специальные классы символов
| Класс | Эквивалент | Описание |
|---|---|---|
d |
[0-9] |
Любая цифра |
D |
[^0-9] |
Всё, кроме цифр |
w |
[a-zA-Z0-9_] |
Любая буква, цифра или подчёркивание |
W |
[^a-zA-Z0-9_] |
Всё, кроме букв, цифр и подчёркивания |
s |
[
f] |
Любой пробельный символ |
S |
[^
f] |
Всё, кроме пробельных символов |
Как Go обрабатывает регулярные выражения
В отличие от некоторых других языков, Go использует RE2 — библиотеку от Google. Её главная особенность: она гарантирует линейное время выполнения. Это значит, что даже самое хитрое регулярное выражение не сможет «зависнуть» на неожиданных данных. Однако за это приходится платить — некоторые возможности (например, обратные ссылки) недоступны.
Запомните: В Go регулярное выражение сначала компилируется (превращается в понятный машине код), а потом уже применяется к тексту. Компиляция — дорогая операция, поэтому старайтесь компилировать выражение один раз и использовать его многократно.
Синтаксис в Go (Golang): все формы и варианты
Пакет regexp предоставляет два основных способа работы: функции-утилиты (для простых случаев) и методы скомпилированного объекта (для производительной работы).
Функции верхнего уровня (для одноразового использования)
| Функция | Что делает | Пример |
|---|---|---|
regexp.MatchString(pattern, s) |
Проверяет, есть ли в строке s хотя бы одно совпадение с шаблоном |
regexp.MatchString(`d+`, "abc123") → true |
regexp.Match(pattern, b) |
То же самое, но для среза байтов | regexp.Match(`d+`, []byte("abc123")) |
regexp.QuoteMeta(s) |
Экранирует все спецсимволы в строке | regexp.QuoteMeta("1+1=2") → "1\+1=2" |
Методы скомпилированного регулярного выражения
Сначала компилируем:
// MustCompile паникует при ошибке (удобно для констант)
re := regexp.MustCompile(`d+`)
// Compile возвращает ошибку (безопасный вариант)
re, err := regexp.Compile(`d+`)
if err != nil {
log.Fatal(err)
}
Теперь у нас есть объект re, с которым можно работать:
| Метод | Что возвращает | Описание |
|---|---|---|
re.MatchString(s) |
bool | Есть ли совпадение в строке? |
re.FindString(s) |
string | Первое совпадение |
re.FindAllString(s, n) |
[]string | Все совпадения (n — сколько найти, -1 — все) |
re.FindStringIndex(s) |
[]int | Индексы первого совпадения [start, end] |
re.FindStringSubmatch(s) |
[]string | Первое совпадение и все группы захвата |
re.ReplaceAllString(s, repl) |
string | Замена всех совпадений |
re.Split(s, n) |
[]string | Разделение строки по шаблону |
re.String() |
string | Возвращает исходный шаблон |
Флаги (модификаторы)
В Go флаги указываются в начале шаблона в скобках (?флаг):
| Флаг | Описание | Пример |
|---|---|---|
(?i) |
Игнорировать регистр | (?i)go найдёт "Go", "GO", "go" |
(?m) |
Многострочный режим (^ и $ работают для каждой строки) | (?m)^Привет |
(?s) |
Точка включает перевод строки | (?s)привет.мир |
Примеры кода — от простого к сложному
Пример 1: Проверка, содержит ли строка цифры
package main
import (
"fmt"
"regexp"
)
func main() {
// Создаём регулярное выражение: d — любая цифра, + — одна или более
re := regexp.MustCompile(`d+`)
// Проверяем строку
text := "Привет, мне 25 лет!"
result := re.MatchString(text)
fmt.Println("Содержит цифры:", result) // Выведет: true
// Проверяем строку без цифр
text2 := "Привет, мир!"
result2 := re.MatchString(text2)
fmt.Println("Содержит цифры:", result2) // Выведет: false
}
Объяснение: d+ означает «одна или более цифр подряд». MatchString возвращает true, если нашёл хотя бы одно совпадение.
Пример 2: Поиск всех email-адресов
package main
import (
"fmt"
"regexp"
)
func main() {
// Шаблон для email: буквы, цифры, точки, подчёркивания @ буквы.буквы
// w — буква/цифра/подчёркивание, [w.] — добавляем точку
// + — один или более раз, . — экранированная точка
pattern := `[w.]+@[w.]+.w+`
re := regexp.MustCompile(pattern)
text := "Мои контакты: john@example.com и alice@mail.ru. Также есть support@my-site.org"
// FindAllString ищет все совпадения, -1 означает "найти все"
emails := re.FindAllString(text, -1)
fmt.Println("Найденные email-адреса:")
for i, email := range emails {
fmt.Printf("%d. %s
", i+1, email)
}
}
Объяснение: [w.]+ — это набор символов: буквы, цифры, подчёркивание и точка. Плюс означает, что их может быть много. . — это экранированная точка (просто точка, а не «любой символ»).
Пример 3: Извлечение данных с помощью групп захвата
package main
import (
"fmt"
"regexp"
)
func main() {
// Шаблон для извлечения имени и возраста
// (w+) — первая группа (имя), (d+) — вторая группа (возраст)
pattern := `Меня зовут (w+), мне (d+) лет`
re := regexp.MustCompile(pattern)
text := "Меня зовут Иван, мне 25 лет"
// FindStringSubmatch возвращает всё совпадение и группы
matches := re.FindStringSubmatch(text)
if len(matches) > 0 {
fmt.Println("Полное совпадение:", matches[0]) // "Меня зовут Иван, мне 25 лет"
fmt.Println("Имя:", matches[1]) // "Иван"
fmt.Println("Возраст:", matches[2]) // "25"
}
}
Объяснение: Круглые скобки создают группы захвата. matches[0] — это всё найденное совпадение, matches[1] — первая группа, matches[2] — вторая.
Пример 4: Замена всех дат из формата ГГГГ-ММ-ДД в ДД.ММ.ГГГГ
package main
import (
"fmt"
"regexp"
)
func main() {
// Шаблон: 4 цифры, дефис, 2 цифры, дефис, 2 цифры
pattern := `(d{4})-(d{2})-(d{2})`
re := regexp.MustCompile(pattern)
text := "Сегодня 2024-01-15, а завтра 2024-01-16"
// В замене $1, $2, $3 — это ссылки на группы захвата
result := re.ReplaceAllString(text, "$3.$2.$1")
fmt.Println("Было:", text)
fmt.Println("Стало:", result)
// Выведет: "Сегодня 15.01.2024, а завтра 16.01.2024"
}
Объяснение: $1, $2, $3 — это ссылки на захваченные группы. Мы переставляем их местами, чтобы получить нужный формат.
Пример 5: Проверка сложности пароля
package main
import (
"fmt"
"regexp"
)
func checkPassword(password string) string {
// Проверяем длину (минимум 8 символов)
if len(password) < 8 {
return "Слишком короткий пароль"
}
// Проверяем наличие хотя бы одной цифры
hasDigit := regexp.MustCompile(`d`).MatchString(password)
if !hasDigit {
return "Пароль должен содержать хотя бы одну цифру"
}
// Проверяем наличие хотя бы одной заглавной буквы
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
if !hasUpper {
return "Пароль должен содержать хотя бы одну заглавную букву"
}
// Проверяем наличие хотя бы одной строчной буквы
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
if !hasLower {
return "Пароль должен содержать хотя бы одну строчную букву"
}
// Проверяем наличие спецсимволов
hasSpecial := regexp.MustCompile(`[!@#$%^&*]`).MatchString(password)
if !hasSpecial {
return "Пароль должен содержать хотя бы один спецсимвол (!@#$%^&*)"
}
return "Пароль надёжный!"
}
func main() {
passwords := []string{"123", "password", "Password1", "Pass1!", "SuperPass123!"}
for _, pwd := range passwords {
fmt.Printf("%s → %s
", pwd, checkPassword(pwd))
}
}
Объяснение: Здесь мы используем несколько простых регулярных выражений для проверки разных критериев. Каждое выражение проверяет одну характеристику пароля.
Пример 6: Разделение строки по сложному разделителю
package main
import (
"fmt"
"regexp"
)
func main() {
// Разделяем строку по знакам препинания и пробелам
// [s,.;:!?]+ — один или более пробелов или знаков препинания
pattern := `[s,.;:!?]+`
re := regexp.MustCompile(pattern)
text := "Привет, мир! Как дела? Всё хорошо; давай... до встречи."
// Split разделяет строку, -1 означает "все части"
words := re.Split(text, -1)
fmt.Println("Исходный текст:", text)
fmt.Println("Разделено на слова:")
for i, word := range words {
if word != "" { // Пропускаем пустые строки
fmt.Printf("%d. %s
", i+1, word)
}
}
}
Объяснение: Split работает как strings.Split, но использует регулярное выражение для определения разделителя. Это гораздо гибче, чем разделение по одному символу.
Пример 7: Поиск и подсветка всех HTML-тегов
package main
import (
"fmt"
"regexp"
)
func main() {
// Шаблон для HTML-тегов: <...>
// < — открывающая скобка
// [^>]* — любые символы, кроме >
// > — закрывающая скобка
pattern := `<[^>]*>`
re := regexp.MustCompile(pattern)
html := `<div class="content">
<h1>Заголовок</h1>
<p>Текст с <b>жирным</b> словом</p>
</div>`
// Находим все теги
tags := re.FindAllString(html, -1)
fmt.Println("Найденные HTML-теги:")
for i, tag := range tags {
fmt.Printf("%d. %s
", i+1, tag)
}
// Подсчитываем количество тегов
fmt.Printf("
Всего найдено тегов: %d
", len(tags))
// Удаляем все теги (оставляем только текст)
cleanText := re.ReplaceAllString(html, "")
fmt.Println("
Текст без тегов:")
fmt.Println(cleanText)
}
Объяснение: <[^>]*> — это классический шаблон для поиска HTML-тегов. [^>]* означает «ноль или более любых символов, кроме >». Это гарантирует, что мы не выйдем за пределы одного тега.
Сравнение с Python: чем похоже и чем отличается
Если вы уже знакомы с регулярными выражениями в Python, вот ключевые различия:
| Характеристика | Go (regexp) | Python (re) |
|---|---|---|
| Движок | RE2 (линейное время, нет обратных ссылок) | PCRE (поддерживает обратные ссылки, но может зависнуть) |
| Компиляция | Обязательна для многократного использования | Автоматически кэширует последние 512 выражений |
| Синтаксис | Почти идентичен | Почти идентичен |
| Группы захвата | $1, $2 в замене |
1, 2 в замене |
| Именованные группы | (?P<name>...) |
(?P<name>...) |
| Флаги | Внутри шаблона: (?i) |
Параметром: re.IGNORECASE |
| Обратные ссылки | НЕ поддерживаются | Поддерживаются: 1 |
| Lookahead/lookbehind | НЕ поддерживаются | Поддерживаются: (?=...), (?<=...) |
Важное отличие: В Go нет lookahead и lookbehind. Если вам нужно проверить, что после совпадения идёт определённый символ, но не включать его в результат, придётся использовать группы захвата и обрабатывать результат вручную.
Типичные ошибки и подводные камни
1. Забыли экранировать спецсимволы
// НЕПРАВИЛЬНО: точка означает "любой символ"
re := regexp.MustCompile(`google.com`) // Найдёт "googleXcom"
// ПРАВИЛЬНО: экранируем точку
re := regexp.MustCompile(`google.com`) // Найдёт только "google.com"
2. Жадность квантификаторов
text := "<div>текст1</div><div>текст2</div>"
// Жадный режим: * захватит как можно больше
reGreedy := regexp.MustCompile(`<div>.*</div>`)
fmt.Println(reGreedy.FindString(text))
// Выведет: "<div>текст1</div><div>текст2</div>" (всё целиком)
// Ленивый режим: *? захватит как можно меньше
reLazy := regexp.MustCompile(`<div>.*?</div>`)
fmt.Println(reLazy.FindString(text))
// Выведет: "<div>текст1</div>" (только первое совпадение)
3. Компиляция внутри цикла
// НЕПРАВИЛЬНО: компилируем на каждой итерации
for _, text := range texts {
re := regexp.MustCompile(`d+`) // ОЧЕНЬ МЕДЛЕННО!
re.MatchString(text)
}
// ПРАВИЛЬНО: компилируем один раз
re := regexp.MustCompile(`d+`)
for _, text := range texts {
re.MatchString(text) // Быстро!
}
4. Неправильное использование якорей
// ^ и $ работают для ВСЕЙ строки, а не для каждой строки
text := "строка1
строка2
строка3"
// Найдёт только если "строка1" в самом начале
re := regexp.MustCompile(`^строка1`)
fmt.Println(re.FindString(text)) // "строка1"
// Чтобы искать в начале каждой строки, используем (?m)
reMulti := regexp.MustCompile(`(?m)^строка`)
fmt.Println(reMulti.FindAllString(text, -1)) // ["строка1", "строка2", "строка3"]
5. Путаница между MatchString и FindString
re := regexp.MustCompile(`abc`)
// MatchString проверяет, есть ли совпадение ГДЕ-УГОДНО в строке
fmt.Println(re.MatchString("xyzabcxyz")) // true
// FindString возвращает ПЕРВОЕ совпадение
fmt.Println(re.FindString("xyzabcxyz")) // "abc"
// MatchString НЕ проверяет, что строка целиком соответствует шаблону!
// Для полного соответствия используйте якоря:
reFull := regexp.MustCompile(`^abc$`)
fmt.Println(reFull.MatchString("abc")) // true
fmt.Println(reFull.MatchString("xyzabc")) // false
Лучшие практики и советы профессионалов
1. Всегда используйте сырые строки
В Go сырые строки (в обратных кавычках) не обрабатывают escape-последовательности. Это делает регулярные выражения гораздо читаемее:
// Плохо: нужно удваивать обратные слеши
re := regexp.MustCompile("\d+\.\d+")
// Хорошо: сырая строка
re := regexp.MustCompile(`d+.d+`)
2. Компилируйте один раз, используйте много раз
// Создаём глобальную переменную для часто используемого выражения
var emailRegex = regexp.MustCompile(`^[w.]+@[w.]+.w+$`)
func isValidEmail(email string) bool {
return emailRegex.MatchString(email)
}
3. Используйте MustCompile для констант
// Если выражение точно корректно (например, задано в коде)
var phoneRegex = regexp.MustCompile(`^+7d{10}$`)
// Если выражение приходит из пользовательского ввода
userPattern := getUserInput()
re, err := regexp.Compile(userPattern)
if err != nil {
// Обрабатываем ошибку
return
}
4. Тестируйте регулярные выражения
func TestEmailRegex(t *testing.T) {
re := regexp.MustCompile(`^[w.]+@[w.]+.w+$`)
tests := []struct {
email string
valid bool
}{
{"user@example.com", true},
{"user.name@example.co.uk", true},
{"@example.com", false},
{"user@.com", false},
{"user@example", false},
}
for _, tt := range tests {
if re.MatchString(tt.email) != tt.valid {
t.Errorf("Email %s: ожидалось %v", tt.email, tt.valid)
}
}
}
5. Не пытайтесь парсить HTML регулярками
Золотое правило: Регулярные выражения не подходят для парсинга HTML, XML или JSON. Используйте специальные парсеры. Регулярки хороши для простых текстовых шаблонов, но не для вложенных структур.
6. Документируйте сложные выражения
// Шаблон для российского номера телефона:
// ^ — начало строки
// +7 — код страны
// d{3} — код оператора (3 цифры)
// d{7} — номер (7 цифр)
// $ — конец строки
var phoneRegex = regexp.MustCompile(`^+7d{3}d{7}$`)
Вопросы для самопроверки
-
Вопрос: Что вернёт
regexp.MustCompile(`d+`).MatchString("abc")?Ответ:
false, потому что в строке "abc" нет цифр. -
Вопрос: Чем отличается
FindStringотFindAllString?Ответ:
FindStringвозвращает только первое совпадение, аFindAllString— все совпадения в виде среза строк. -
Вопрос: Что