Строки — одна из самых часто используемых структур данных в любом языке программирования. И в Go они повсюду: от простых приветствий вроде "Hello, World!" до сложных парсеров, логгеров и сетевых протоколов. На первый взгляд, с ними всё просто: создал, склеил, обрезал — и пошёл дальше. Но как это часто бывает, под простотой скрываются тонкости, игнорирование которых может привести к ошибкам и снижению производительности.

Go предлагает удобные, но специфичные механизмы работы со строками. Они тесно связаны с понятиями Unicode, UTF-8, рун, неизменяемости и байтовых представлений. Понимание этих механизмов позволяет не только избежать типичных подводных камней, но и писать более эффективный код.

Что такое строка в Go?

В Go строка — это неизменяемый тип данных, который представляет собой указатель на последовательность байтов и длину этой последовательности. То есть, внутри Go строка — это просто набор байтов. И да, это не обязательно UTF-8 , хотя большинство строковых литералов в коде используют именно эту кодировку.

s := "hello"
fmt.Println(len(s)) // 5

Но если добавить символы из других языков или эмодзи, как сразу всё становится интереснее:

s := ""
fmt.Println(len(s)) // 3

Здесь len(s) возвращает количество байтов , а не символов. Это важно. Если вам нужно узнать, сколько реальных символов в строке, используйте пакет unicode/utf8:

utf8.RuneCountInString(s) // вернёт 1

Руны

Чтобы правильно работать с юникодом в Go, нужно понять, что такое руна (rune) . Это псевдоним для int32, и он представляет кодовую точку Unicode — то есть конкретный символ в юникод.

Разница между руной и байтом — огромная. Один символ может занимать от одного до четырех байтов. Поэтому, если работать с текстом, содержащим неанглоязычные символы, игнорировать это нельзя.

Неправильная итерация по строке

Допустим если написать так:

s := "hêllo"
for i := range s {
    fmt.Printf("position %d: %c\n", i, s[i])
}

И ожидать красивые символы. Но вместо этого получается что-то вроде:

position 0: h
position 1: Ã
position 3: l
...

Почему так? Потому что s[i] возвращает байт , а не руну. А символ ê занимает два байта. Такой цикл перебирает индексы байтов , а не рун.

Правильный способ:

for i, r := range s {
    fmt.Printf("position %d: %c\n", i, r)
}

Теперь r — это сама руна, и вывод будет корректным.

Если нужно обратиться к третьей руне в строке, нужно преобразовать строку в срез рун:

runes := []rune(s)
fmt.Printf("%c\n", runes[2]) // Третья руна

Функции Trim

Функции TrimRight, TrimLeft, TrimPrefix, TrimSuffix могут запутать. Например:

strings.TrimRight("123oxo", "xo") // Вернёт "123"

Можно по ошибки ожидать "123o", но TrimRight удаляет все завершающие символы, входящие во множество "xo".

Если нужно удалить строго суффикс , используйте TrimSuffix:

strings.TrimSuffix("123oxo", "xo") // Теперь вернёт "123o"

Аналогично с префиксами: TrimPrefix удаляет строго заданную подстроку в начале.

Медленная конкатенация строк

Допустим, есть список строк, которые нужно объединить. Неверное решение:

func concat(values []string) string {
    s := ""
    for _, v := range values {
        s += v
    }
    return s
}

Проблема здесь в том, что каждое += создаёт новую строку. Это дорого, особенно если строк много.

Лучший способ — использовать strings.Builder:

func concat(values []string) string {
    var sb strings.Builder
    for _, v := range values {
        sb.WriteString(v)
    }
    return sb.String()
}

Если заранее известен размер результата, можно использоватьsb.Grow(totalLength), это ускорит работу.

Лишние преобразования между []byte и string

Многие привыкли работать со строками, но в Go большая часть операций ввода-вывода работает с []byte. Преобразование туда-сюда это лишняя нагрузка.

Вместо:

s := string(bytes.TrimSpace([]byte(data)))

Лучше работать напрямую с байтами:

b := bytes.TrimSpace(data)

Пакет bytes содержит аналоги всех популярных функций из strings: Split, Contains, Index, TrimSpace и другие.

Утечки памяти через подстроки

Это одна из самых неочевидных проблем. Допустим, у вас есть длинная строка, из которой вы берёте подстроку:

uuid := log[:36]

На первый взгляд — всё ок. Но в Go подстрока ссылается на ту же область памяти , что и исходная строка. То есть, даже если хранить всего 36 байт, GC не сможет очистить всю исходную строку, пока она не будет использована.

Чтобы избежать утечки, нужно сделать копию:

uuid := string([]byte(log[:36]))

Или, начиная с Go 1.18:

uuid := strings.Clone(log[:36])

Заключение

Работа со строками в Go может показаться простой на первый взгляд, но за этой простотой скрываются тонкости, которые могут серьёзно повлиять как на производительность, так и на корректность кода. Понимание рун, правильная работа с итерацией, аккуратное использование функций обрезки, эффективная конкатенация и предотвращение утечек памяти - навыки, которые должен освоить каждый разработчик на Go.

Не стоит пренебрегать возможностью работать напрямую с []byte, особенно когда речь идёт о вводе-выводе или обработке больших объёмов данных. При использовании подстрок нужно помнить, что они могут ссылаться на исходную строку целиком — и если хранить только её часть, это может стоить лишней памяти.

Что нужно запомнить:

  • Строка — это последовательность байтов.

  • Руна — это символ Unicode.

  • Использование range для правильной итерации.

  • Отличие TrimRight и TrimSuffix.

  • Для конкатенации — strings.Builder.

  • Лучше избегать лишних преобразований между []byte и string.

  • Делать копии подстрок, чтобы не было утечек памяти.

Комментарии (1)


  1. SolidSnack
    22.05.2025 06:16

    Хорошо бы всем понимать как работают строки в языке которым они пользуются :) В go просто строгая типизация и по этому такие вещи приходится учитывать.