Строки — одна из самых часто используемых структур данных в любом языке программирования. И в 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
.Делать копии подстрок, чтобы не было утечек памяти.
SolidSnack
Хорошо бы всем понимать как работают строки в языке которым они пользуются :) В go просто строгая типизация и по этому такие вещи приходится учитывать.