Строки — одна из самых часто используемых структур данных в любом языке программирования. И в 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.
- Делать копии подстрок, чтобы не было утечек памяти. 
Комментарии (3)
 - gudvinr22.05.2025 06:16- Эта статья чуть ли не копипаст из блога 12 летней давности: - https://go.dev/blog/strings 
 - Sly_tom_cat22.05.2025 06:16- В статье не затронуты важные моменты: 
 1. константные строки - те самые что var := "string" // - тут "string" - константа и все другие строки в коде "string" будут по факту храниться в одном месте. Более того их длинна не будет хранится в памяти (как у строк рантайма) - вместо нее будет подставляться константа при компиляции. Поэтом бессмысленно объявлять именованную строковую константу и использовать ее во всех местах где встречается эта строка. Такую оптимизацию за вас сделает компилятор.
 2. в отличии от строк констант, строки получаемые в райнтайме на хранятся в одном и том же месте даже если они одинаковые. Т.е. 100500 одинаковых строк используемых в исходном коде - это по факту одна строка, а 100500 одинаковых строк полученных в рантайме (парсинг какого-нибудь ответа из API, например) будут представлены в памяти как 100500 отдельных строк. Не хотите такого - используйте uniq.
 
           
 
SolidSnack
Хорошо бы всем понимать как работают строки в языке которым они пользуются :) В go просто строгая типизация и по этому такие вещи приходится учитывать.