Строки, руны и регулярные выражения в Go
Строки выглядят простыми, пока в них только английские буквы и цифры. Но реальные программы работают с именами, адресами, кириллицей, эмодзи, логами, JSON, HTML, командами терминала и данными от пользователей. В этот момент важно понимать, что строка - это не просто "набор букв".
В Go строка хранит байты. Обычно эти байты являются текстом в кодировке UTF-8. Это хорошая современная кодировка, но она означает, что один видимый символ может занимать больше одного байта.
Если это понять с самого начала, многие ошибки станут очевидными: почему len("Привет") возвращает не 6, почему нельзя обрезать пользовательский текст через text[:10], почему range по строке ведет себя не так, как обычный индексный цикл.
string - это последовательность байтов
Тип string в Go неизменяемый. После создания строки нельзя заменить символ внутри нее. Можно только создать новую строку.
text := "Привет" fmt.Println(len(text)) // количество байтов, не букв
Для слова Привет результат будет больше 6, потому что кириллические символы в UTF-8 занимают по два байта.
Если пройти по строке через индекс, вы получите байты:
for i := 0; i < len(text); i++ { fmt.Println(i, text[i]) }
Это полезно, когда вы работаете с протоколами, файлами или бинарными данными. Но для текста, который видит человек, чаще нужен другой подход.
rune и Unicode
rune - это псевдоним для int32. Обычно он представляет Unicode-кодпоинт, то есть один элемент текста.
Для посимвольного обхода используйте range:
for index, r := range text { fmt.Println(index, r, string(r)) }
index здесь все равно байтовый индекс, а r - руна. Это важно: если вам нужен порядковый номер символа, заведите отдельный счетчик.
position := 0 for _, r := range text { fmt.Println(position, string(r)) position++ }
[]byte и []rune
Иногда строку нужно преобразовать:
bytes := []byte(text) runes := []rune(text)
[]byte нужен для файлов, сети, хеширования и низкоуровневой обработки. []rune удобен, когда нужно работать именно с символами:
runes := []rune("Привет") firstThree := string(runes[:3]) fmt.Println(firstThree) // При
Но не превращайте каждую строку в []rune автоматически. Это выделяет память. Используйте такой подход, когда действительно нужна индексация по символам.
Пакет strings
Пакет strings закрывает большинство повседневных задач:
strings.TrimSpace(input) strings.ToLower(name) strings.HasPrefix(command, "/") strings.HasSuffix(filename, ".log") strings.Contains(text, "error") strings.Split(line, ",") strings.Join(parts, ";") strings.ReplaceAll(text, " ", "_")
Эти функции читаются лучше, чем ручные циклы. Если задача звучит как "проверить, содержит ли строка", "убрать пробелы", "разделить по запятой", сначала ищите решение в strings.
Пример нормализации пользовательского ввода:
func normalizeCommand(input string) string { input = strings.TrimSpace(input) input = strings.ToLower(input) return input }
Теперь " HELP " и "help" будут обработаны одинаково.
strings.Builder
Строки неизменяемы. Когда вы делаете result += part в цикле, Go каждый раз создает новую строку. Для маленького кода это не страшно, но для больших циклов лучше использовать strings.Builder.
var builder strings.Builder for _, item := range items { builder.WriteString(item) builder.WriteByte('\n') } result := builder.String()
Мысленно Builder похож на коробку, в которую вы постепенно складываете куски текста. В конце вы закрываете коробку и получаете одну готовую строку.
strconv
Пакет strconv преобразует строки в числа, bool и обратно.
age, err := strconv.Atoi("42") if err != nil { return err } port, err := strconv.ParseInt("8080", 10, 64) price, err := strconv.ParseFloat("12.50", 64) enabled, err := strconv.ParseBool("true")
Обратное преобразование:
text := strconv.Itoa(42)
Всегда обрабатывайте ошибку парсинга. Если пользователь ввел "ten" вместо "10", программа должна дать понятный ответ, а не продолжить с нулевым значением.
fmt.Sprintf
fmt.Sprintf собирает строку по шаблону:
message := fmt.Sprintf("Файл %s: %d ошибок", filename, count)
Частые плейсхолдеры:
| Плейсхолдер | Значение |
|---|---|
%s | строка |
%d | целое число |
%f | число с плавающей точкой |
%.2f | число с двумя знаками после точки |
%v | значение в стандартном виде |
%+v | структура с именами полей |
%#v | Go-представление значения |
%q | строка в кавычках |
Для сообщений пользователю используйте понятные форматы. Для отладки структур часто полезны %+v и %#v.
Регулярные выражения
Регулярное выражение - это шаблон для поиска текста. Оно удобно для логов, валидации, извлечения частей строки и проверки формата.
pattern := regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})\s+(ERROR|INFO)\s+(.+)$`) match := pattern.FindStringSubmatch(line) if match == nil { return nil } date := match[1] level := match[2] message := match[3]
match[0] содержит всю найденную строку. match[1], match[2] и дальше содержат группы в круглых скобках.
MustCompile подходит для шаблона, который известен на этапе написания кода. Если шаблон приходит от пользователя, используйте regexp.Compile:
pattern, err := regexp.Compile(inputPattern) if err != nil { return fmt.Errorf("invalid pattern: %w", err) }
Когда регулярки не нужны
Регулярные выражения мощные, но не всегда лучший инструмент.
Если нужно проверить префикс, используйте:
strings.HasPrefix(value, "user_")
Если нужно разделить CSV-подобную строку, сначала подумайте о strings.Split или специализированном парсере.
Если нужно проверить email в реальном продукте, простая регулярка часто будет либо слишком слабой, либо слишком сложной. Иногда лучше проверить базовую форму и отправить письмо подтверждения.
text/template и html/template
Когда строка становится шаблоном, используйте пакеты text/template или html/template.
tmpl := template.Must(template.New("message").Parse( "Привет, {{.Name}}! У тебя {{.Count}} уведомлений.", )) data := struct { Name string Count int }{ Name: "Alice", Count: 3, } if err := tmpl.Execute(os.Stdout, data); err != nil { return err }
Для HTML используйте html/template: он экранирует опасные символы и помогает не сделать XSS.
Практический пример: парсинг строки лога
func parseLogLine(line string) (string, string, string, bool) { pattern := regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})\s+(\w+)\s+(.+)$`) match := pattern.FindStringSubmatch(strings.TrimSpace(line)) if match == nil { return "", "", "", false } return match[1], match[2], match[3], true }
Такую функцию легко тестировать: передайте правильную строку, пустую строку, строку без даты и строку с лишними пробелами.
Мини-чеклист
- Для байтов используйте
[]byte. - Для символов используйте
runeиrange. - Для простого поиска используйте
strings, не регулярки. - Для чисел используйте
strconv, не ручной парсинг. - Для сборки текста в цикле используйте
strings.Builder. - Для регулярки из кода используйте
regexp.MustCompile. - Для регулярки от пользователя используйте
regexp.Compile. - Для HTML используйте
html/template, неtext/template.