Строка в Go это неизменяемая последовательность байтов. Не символов, а именно байтов. Обычно в ней лежит UTF-8, но язык этого не требует.
Устройство
Внутри строка похожа на слайс без ёмкости: указатель на байты и длина. Присваивание строки копирует только эти два поля, сами байты общие, и это безопасно, потому что их нельзя изменить. s[0] = 'x' не скомпилируется.
s := "привет"
fmt.Println(len(s)) // 12: кириллица занимает 2 байта
fmt.Println(utf8.RuneCountInString(s)) // 6 символов
fmt.Println(s[0]) // 208: это байт, а не буква
Руны и range
rune это int32, код символа Unicode. Цикл range по строке декодирует UTF-8 и выдаёт пары (индекс байта, руна):
for i, r := range "go!ы" {
fmt.Println(i, string(r))
}
// 0 g
// 1 o
// 2 !
// 3 ы
Индексы идут по байтам, поэтому после многобайтного символа скачут. Обычный for i := 0; i < len(s); i++ идёт по байтам и разрезает символы. Битый UTF-8 в range превращается в руну U+FFFD.
Преобразования
b := []byte("abc") // копия байтов, можно менять
r := []rune("абв") // декодирование в руны
s := string(r) // обратно в UTF-8
[]byte(s) и string(b) в общем случае копируют данные. Компилятор умеет обходиться без копии в частных случаях, например в ключе мапы m[string(b)] или в сравнении, но на это не стоит опираться в логике.
Чтобы развернуть строку с кириллицей, работайте с []rune, а не с байтами. Для эмодзи из нескольких кодовых точек и этого мало: там нужны графемные кластеры, стандартная библиотека их не умеет.
Конкатенация
s += x в цикле каждый раз создаёт новую строку и копирует всё накопленное: O(n²). Правильно:
var sb strings.Builder
for _, w := range words {
sb.WriteString(w)
}
res := sb.String() // без лишней копии
strings.Join для готового слайса, fmt.Sprintf для форматирования (медленнее, зато удобно), bytes.Buffer когда нужны и чтение, и запись.
Полезное из strings
strings.Fields режет по пробельным символам, Split по разделителю (Split("", ",") даёт слайс из одной пустой строки), TrimSpace, HasPrefix, Cut (Go 1.18+, удобный разбор key=value), EqualFold для сравнения без регистра. Для чисел strconv.Atoi, ParseInt, Itoa, FormatFloat.
Сравнение
Строки сравниваются == и < побайтно, лексикографически. Сравнение без регистра через strings.EqualFold, а не через ToLower с двух сторон (лишние аллокации).
Частые ошибки
- Считать
len(s)числом символов. - Индексировать строку и ждать букву, а получать байт.
- Собирать строку через
+=в большом цикле. - Писать
string(n)для числа вместоstrconv.Itoa(n).
Что спросят на собесе
- Что такое строка в Go и почему она неизменяема.
- Чем
byteотличается отrune, что вернётlenдля кириллицы. - Как
rangeидёт по строке. - Как эффективно склеить много строк и почему
+=плохо. - Копирует ли
[]byte(s)данные.