Строки (string)
1. Фундаментальные свойства строк
Свойство 1: Строки — это неизменяемые (immutable) последовательности байтов
Это самое важное, что нужно знать о строках в Go.
- Последовательность байтов: Строка внутри — это просто набор байтов. Она не хранит информацию о кодировке, но стандартной практикой является использование UTF-8.
- Неизменяемость (Immutability): После того как строка создана, ни один из её байтов изменить нельзя. Любая операция, которая "модифицирует" строку (например, конкатенация или замена символа), на самом деле создает совершенно новую строку в памяти.
Аналогия: Представьте себе фотографию. Вы не можете изменить человека на уже напечатанном снимке. Вы можете только взять новый снимок с изменениями. Так же и со строками.
name := "Alice"
// Попытка изменить первый символ приведет к ошибке компиляции
// name[0] = 'a' // Error: cannot assign to name[0]
// Конкатенация создает НОВУЮ строку
greeting := "Hello, " + name // В памяти теперь две строки: "Alice" и "Hello, Alice"
Почему это хорошо?
- Безопасность: Вы можете спокойно передавать строки в функции, зная, что они не будут изменены "под капотом". Это предотвращает множество скрытых ошибок.
- Производительность: Строки можно безопасно использовать в качестве ключей в
map, так как они гарантированно не изменятся.
Свойство 2: len() возвращает количество байтов, а не символов
Это самая частая ловушка для новичков. Поскольку Go использует кодировку UTF-8, символы, не входящие в ASCII (например, кириллица или эмодзи), могут занимать от 2 до 4 байтов.
str1 := "Go"
fmt.Println(len(str1)) // Вывод: 2 (2 символа, 2 байта)
str2 := "Привет"
fmt.Println(len(str2)) // Вывод: 12 (6 символов, но 12 байт, т.к. каждый символ кириллицы в UTF-8 занимает 2 байта)
str3 := "🚀"
fmt.Println(len(str3)) // Вывод: 4 (1 символ, но 4 байта)
Как правильно посчитать символы (руны)?
Используйте пакет unicode/utf8.
import "unicode/utf8"
str := "Привет"
fmt.Println(utf8.RuneCountInString(str)) // Вывод: 6
2. Итерация по строке
Есть два способа перебора строки, и очень важно понимать разницу между ними.
Неправильный способ: Итерация по байтам
Классический цикл for с индексом будет перебирать байты, что приведет к некорректным результатам для многобайтовых символов.
str := "Привет"
for i := 0; i < len(str); i++ {
fmt.Printf("%c ", str[i]) // Печатаем символ по байту
}
// Вывод: П р и в е т (неверный вывод, может отличаться в зависимости от терминала)
// Вы получите некорректные символы, т.к. каждый символ кириллицы состоит из ДВУХ байтов
Правильный способ: Итерация по рунам с помощью range
Цикл for ... range специально спроектирован для корректной работы со строками в UTF-8. На каждой итерации он декодирует следующую руну и возвращает её вместе с начальным индексом (в байтах).
str := "Go-🚀"
// index — это начальный индекс байта руны в строке
// char — это сама руна (тип rune, который является синонимом int32)
for index, char := range str {
fmt.Printf("Символ: %c, начинается с байта %d\n", char, index)
}
/*
Вывод:
Символ: G, начинается с байта 0
Символ: o, начинается с байта 1
Символ: -, начинается с байта 2
Символ: 🚀, начинается с байта 3 (обратите внимание, следующий индекс будет 7)
*/
3. Основные операции со строками
Поскольку строки неизменяемы, для работы с ними используется стандартный пакет strings.
Конкатенация (объединение)
- Простой способ (
+): Удобен для 2-3 строк.s := "hello" + " " + "world" - Эффективный способ (
strings.Builder): Если нужно объединить много строк (например, в цикле),+будет очень неэффективным, так как на каждой итерации создается новая строка.strings.Builderработает гораздо быстрее, выделяя память под результат заранее.
import "strings"
var builder strings.Builder
parts := []string{"a", "b", "c", "d"}
for _, part := range parts {
builder.WriteString(part) // Добавляем часть строки без создания промежуточных строк
}
result := builder.String() // Получаем итоговую строку один раз
fmt.Println(result) // Вывод: abcd
Другие полезные функции из пакета strings
import "strings"
s := " Hello, World! Hello! "
// Проверка на содержание
fmt.Println(strings.Contains(s, "World")) // true
// Замена
result := strings.ReplaceAll(s, "Hello", "Hi")
fmt.Println(result) // " Hi, World! Hi! "
// Смена регистра
fmt.Println(strings.ToUpper(s)) // " HELLO, WORLD! HELLO! "
fmt.Println(strings.ToLower(s)) // " hello, world! hello! "
// Удаление пробелов по краям
trimmed := strings.TrimSpace(s)
fmt.Println(trimmed) // "Hello, World! Hello!"
// Разделение строки на слайс
parts := strings.Split(trimmed, ", ")
fmt.Println(parts[0], parts[1]) // "Hello" "World! Hello!"
4. Raw String Literals (Сырые строковые литералы)
Строки в Go можно определять двумя способами:
- В двойных кавычках
"": Интерпретируют escape-последовательности, такие как\n(новая строка) или\t(табуляция). - В обратных кавычках (backticks) ` `: Игнорируют все escape-последовательности и могут содержать переносы строк.
- Частота использования: Реже, чем обычные строки, но незаменимы в своих сценариях.
- Когда использовать:
- Многострочный текст: SQL-запросы, шаблоны HTML/JSON.
- Пути к файлам в Windows: Чтобы не экранировать каждый
\. - Регулярные выражения: Которые часто содержат много обратных слэшей.
// 1. Обычная строка
path1 := "C:\\Users\\Admin\\Documents"
// 2. Сырой литерал - гораздо чище
path2 := `C:\Users\Admin\Documents`
// 3. Многострочный JSON
jsonString := `{
"name": "John",
"age": 30
}`
fmt.Println(path1)
fmt.Println(path2)
fmt.Println(jsonString)