Перейти к основному содержимому

Строки (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 можно определять двумя способами:

  1. В двойных кавычках "": Интерпретируют escape-последовательности, такие как \n (новая строка) или \t (табуляция).
  2. В обратных кавычках (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)