Урок по треку Go 6 мин

Строки, байты и руны в Go

Строка как неизменяемые байты, UTF-8, range по строке, руны, strings.Builder и конкатенация.

Строка в 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) данные.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.